Контроль температур горячей воды в ТСЖ с оповещениями в Telegram на базе ESP8266

Опубликовал | 22.11.2018
Последнее время в нашем доме часто стала возникать проблема с горячей водой. В принципе, это началось, когда мы отсудили котельную в общедомовую собственность и начали сами ей распоряжаться, в том числе наняли другую обслуживающую организацию. Потом обслуживающую организацию снова поменяли, стало дешевле, но качество не выросло. И больше всего напрягало, что с момента остановки котельной до момента, когда горячая вода снова нагревалась до нужной температуры, проходило очень много времени — порой до четырех часов. Складывалось ощущение, что диспетчеризация не работала, либо на ее сигналы просто забивал диспетчер, а высылали техника к нам только после телефонного звонка. Приходилось щупать змеевик в ванной и гадать — 5 минут назад было теплее и он остывает или все-таки котельную уже запустили и он нагревается? Бегать в котельную вечером, как вы понимаете, тоже очень лениво.

Наткнулся тут на обзоры всяких датчиков от Sanja, этот добрый человек прислал ссылки на запчасти, что мне надо закупить (плата с модулем WiFi NodeMCU ESP8266 и датчики DS18b20) и даже ролик на ютубе, показывающий, что надо делать и как. В принципе, этих ссылок достаточно для создания аналогичного устройства, но были и сложности, о которых я и хочу рассказать, дабы еще более упросить повторение процесса для таких же новичков в этом деле, как я)

Это мой первый опыт использования подобной техники и программирования контроллеров, поэтому используемые методы пайки или монтирования могут повергнуть в шок бывалых мастеров… Впечатлительных прошу отдалиться от мониторов). Также я не стал заморачиваться с печатью корпуса для устройства, а использовал пластиковую бутылку (почти), как научила меня передача «Очумелые ручки» в свое время :)

Итак, нам понадобится:

  • Плата с модулем WiFi NodeMCU ESP8266 ~220 руб
  • Датчики Dallas DS18b20 по ~80 руб за штуку
  • Резистор 4к7 маломощный ~5 руб
  • Разъемы «мама» для подключения к ножкам Arduino и подобным ~120руб за 40 штук
  • WiFi роутер (если в предполагаемом для установки помещении его еще нет) ~400 руб б/у
  • Блок питания USB (1А, говорят, достаточно, но я брал на 2А) и кабель microUSB для программирования (при подключении к компу) и питания ~200 руб

Сначала параллельно соединяем все датчики, которые нам нужны, в одно целое

по такой схеме:

Получилось примерно так:

Потом, конечно, это все тщательно замотано изолентой/термоусадкой.

Подключаем к пинам по схеме, плату подсоединяем USB-кабелем к компьютеру, запускаем свежеустановленный Arduino 1.8.2 и настраиваем его, как описано тут. Порт меняем на COM4 (если плата уже подключена).

Распаковываем архив в папку, где у вашей Arduino лежат скетчи. В комплекте уже есть библиотеки, которые было так тяжело найти подходящие, пол дня убил на это.
Загружаем сам скетч sketch_esp8266dallas в среду разработки.
Меняем там на строках 14 и 15 название WiFi сети и пароль к ней, а также на 45 строке адрес сервера, на котором будут сохраняться передаваемые параметры. Серверная часть (PHP) описана ниже.
К слову, мы могли бы уже здесь отправлять уведомления в Telegram, если температуры ниже заданного уровня, немного изменив код отправки GET-запроса, но Telegram у нас в России под запретом, поэтому существуют некоторые проблемы с использованием его API. Альтернативный вариант — использовать API от sms.ru, но это уже совсем другая история. Главное, что сюда можно вписать абсолютно любое обращение к какому угодно сервису, будь то «народный мониторинг» или любой другой.

Пробуем скомпилировать скетч через меню «Скетч — Проверить/Компилировать», если все хорошо и ошибок нет, то можно загружать в плату «Скетч — Загрузка».

Открываем меню «Инструменты — Монитор порта» и видим примерно такую картину:

А если у вас картина другая и плата WiFi-соединение поднимает, но запросы нифига не идут, то скорее всего роутер находится слишком далеко и сигнал слабый. У меня роутер стоял в соседней комнате (!) и я пару часов убил на выяснение, почему же не работает сервис Blink или любой другой. Когда ты этим занимаешься первый раз, сложно понять, что плате просто не хватает мощности сигнала, ведь к сети то она подцепиться смогла, причем с первого раза. И именно по этой причине пришлось купить дополнительный роутер, потому что в подвале, где я хотел расположить датчик, ситуация с WiFi была бы еще хуже. Я даже посмотрел кучку обзоров на разные вариации этой платы и анализ мощности приема WiFi-сигнала, а также возможность использования дополнительных антенн, но найденные примеры не демонстрировали существенного эффекта.
Тут мне пришла в голову мысль, что вместо WiFi-платы можно было бы использовать сразу модуль с RJ-45 на борту, но было уже поздно :)

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

У меня, как у любого порядочного веб-программиста, имеется с пяток хостинговых площадок в распоряжении, поэтому вопроса о том, где разместить серверную часть, не возникло — уже работал в ТСЖ сайт на вордпрессе. Но есть и варианты бесплатного хостинга с PHP и MySQL, я ими не пользовался и ссылок ставить не буду, но найти их легко в Яндексе.

Далее нам нужно через PHPMyAdmin создать табличку в любой имеющейся или новой базе с названием temperatures и полями: datatime (тип DATETIME), t1 (DECIMAL), t2 (DECIMAL), t3 (DECIMAL), t4 (DECIMAL).

Распаковываем архив серверной части через ftp у себя на сервере и правим в файлах index.php и json.php параметры соединения с базой данных и в index.php внешний адрес скрипта для получения данных.

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

index.php
<?php  // Параметры соединения с базой данных  $db_server = 'localhost';  $db_name = '*****';  $db_user = '*****';  $db_passwd = '******';    // Подключение к базе данных  $db_connection = @mysql_connect($db_server, $db_user, $db_passwd);  if (!$db_connection || !@mysql_select_db($db_name, $db_connection))  {  	echo "Error during SQL connection";  }  mysql_select_db($db_name, $db_connection);    // Сохранение в базу переданных с контроллера данных  if(isset($_GET['t1']) && isset($_GET['t2']) && isset($_GET['t3']) && isset($_GET['t4'])) {  	$t1 = floatval($_GET['t1']);  	$t2 = floatval($_GET['t2']);  	$t3 = floatval($_GET['t3']);  	$t4 = floatval($_GET['t4']);  	if($t1>0 && $t2>0 && $t3>0&& $t4>0) {  		$result = mysql_query("  			INSERT INTO `temperatures` (datatime,t1,t2,t3,t4)   			VALUES (now(), $t1, $t2, $t3, $t4)");  		echo mysql_error();	  		  		// Если температуры ниже минимума, отправим сообщение в Telegram  		if($t4<40 || $t2<35 || $t1<50 || $t3<50) {  			include 'tele.php';  			message_to_telegram("ГВС_П: $t4, ГВС_О: $t2, Котельная_П: $t1, Котельная_О: $t3");  		}  	}  	  	die();  }  ?>    <html>  <head>  	<title>Температурные данные дома №*** по ул.*********</title>  	<link href="/temp/style.css" rel="stylesheet">  	<link href="/temp/jquery.datetimepicker.min.css" rel="stylesheet">  	<script type="text/javascript" src="/temp/jquery-3.3.1.min.js"></script>  	<script type="text/javascript" src="/temp/moment.js"></script>  	<script type="text/javascript" src="/temp/jquery.datetimepicker.full.min.js"></script>  	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>  	<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">  </head>  <body>   	<div class='current row'>  		<div class='col time'></div>  		<div class='t1 col'>ГВС, подача:   <b></b></div>  		<div class='t2 col'>ГВС, обратка:   <b></b></div>  		<div class='t3 col'>Котельная, подача:   <b></b></div>  		<div class='t4 col'>Котельная, обратка:   <b></b></div>  	</div>   	<div class="row selectperiod">  		<div class="col"><input type="text" id="datastart" value="" class="form-control"/></div>  		<div class="col"><input type="text" id="dataend" value="" class="form-control"/></div>  		<div class="col">Точность: <select id="intervalMinutes">  			<option value="1">1 мин</option>  			<option value="5">5 мин</option>  			<option value="15">15 мин</option>  			<option value="30" selected>30 мин</option>  			<option value="6">1 час</option>  			</select>  		</div>  	</div>    	<div id="chartContainer" style="position: relative; height:40vh; width:90vw;margin: 0 auto;">  		<canvas id="myChart"></canvas>  	</div>  	<script>  	// Создание и отрисовка графика  	function getChart(dataFromJSON) {  		var dates = [], t1arr = [], t2arr = [], t3arr = [], t4arr = [];  		dataFromJSON.forEach(function(item, i, arr) {  			dates.push(item.datatime);  			t1arr.push(item.t1);  			t2arr.push(item.t2);  			t3arr.push(item.t3);  			t4arr.push(item.t4);  		});  	  		var ctx = document.getElementById("myChart").getContext('2d');  		window.myChart = new Chart(ctx, {  			type: 'line',  			data: {  				labels: dates,  				datasets: [{  					label: 'ГВС, подача',  					data: t1arr,  					borderColor: '#ffc700',  					backgroundColor: 'rgba(255,255,255,0)'  				},{  					label: 'ГВС, обратка',  					data: t2arr,  					borderColor: '#3bd100',  					backgroundColor: 'rgba(255,255,255,0)'  				},{  					label: 'Контур котельной, подача',  					data: t3arr,  					borderColor: '#ff0000',  					backgroundColor: 'rgba(255,255,255,0)'  				},{  					label: 'Контур котельной, обратка',  					data: t4arr,  					borderColor: '#ee00ff',  					backgroundColor: 'rgba(255,255,255,0)'  				}]  			},  			options: {  				responsive: true,  				//maintainAspectRatio: false,  				legend: {  					display: false  				},  				scales: {  					xAxes: [{  						type: 'time',  						time: {  							unit: 'minute'  						}  					}]  				}  			}  		});  	}  	  	// Обновление графика данными  	function updateChart(dataFromJSON) {  		var dates = [], t1arr = [], t2arr = [], t3arr = [], t4arr = [];  		dataFromJSON.forEach(function(item, i, arr) {  			dates.push(item.datatime);  			t1arr.push(item.t1);  			t2arr.push(item.t2);  			t3arr.push(item.t3);  			t4arr.push(item.t4);  		});  		  		window.myChart.data.labels = dates;  		window.myChart.data.datasets[0].data = t1arr;  		window.myChart.data.datasets[1].data = t2arr;  		window.myChart.data.datasets[2].data = t3arr;  		window.myChart.data.datasets[3].data = t4arr;  		window.myChart.update();  	}  	  	// Получение данных для графика  	function doUpdateChart() {  		$.getJSON( "http://адрес вашего сайта и путь к файлу/temp/json.php?datastart="+$("#datastart").val()+"&dataend="+$("#dataend").val()+"&intervalMinutes="+$("#intervalMinutes").val(),  			function( data ) {  				updateChart(data);  			});  	}  	  	// Получение текущих данных  	function doUpdateLastValues() {  		$.getJSON( "http://адрес вашего сайта и путь к файлу/json.php?last=true",  			function( data ) {  				$(".time").html((data[0].datatime+'').replace(' ', '  '));  				$(".t1 b").text(data[0].t1+"°C");  				$(".t2 b").text(data[0].t2+"°C");  				$(".t3 b").text(data[0].t3+"°C");  				$(".t4 b").text(data[0].t4+"°C");  			});  	}  		  	$(function() {  		// Выбор дат  		$.datetimepicker.setLocale('ru');  		$('#datastart,#dataend').datetimepicker({  		  format:'d.m.Y H:i',  		  lang:'ru',  		  minDate:'20.11.2018',  		  maxDate:Date.now(),  		  formatDate:'d.m.Y',  		  dayOfWeekStart: 1,  		  i18n:{  			  ru:{  			   months:[  				'Январь','Февраль','Март','Апрель',  				'Май','Июнь','Июль','Август',  				'Сентябрь','Октябрь','Ноябрь','Декабрь',  			   ],  			   dayOfWeek:[  				"Вс", "Пн", "Вт", "Ср",   				"Чт", "Пт", "Сб",  			   ]  			  }  			 },  		});  				  		// Загрузка текущих данных  		doUpdateLastValues();  		// Заполнение графика  		$.getJSON( "http://адрес вашего сайта и путь к файлу/temp/json.php?datastart="+$("#datastart").val()+"&dataend="+$("#dataend").val()+"&intervalMinutes="+$("#intervalMinutes").val(),  			function( data ) {  				getChart(data);  			});  					  		window.lastDataStart = $("#datastart").val();  		window.lastDataEnd = $("#dataend").val();  		window.intervalMinutes = $("#intervalMinutes").val();  		var winHeight = $(window).height();  		setInterval(function() {  			var changed = false;  			if($("#datastart").val() != window.lastDataStart) {  				changed = true;  				window.lastDataStart = $("#datastart").val();  			}  			if($("#dataend").val() != window.lastDataEnd) {  				changed = true;  				window.lastDataEnd = $("#dataend").val();  			}  			if($("#intervalMinutes").val() != window.intervalMinutes) {  				changed = true;  				window.intervalMinutes = $("#intervalMinutes").val();  			}  			if(changed)  				doUpdateChart();  		}, 1000);  		// Обновлять текущие данные раз в 30 сек  		setInterval(function() {  			doUpdateLastValues();  			doUpdateChart();  		}, 30000);  	});  	</script>    </body>  </html>

json.php
<?php  ini_set('error_reporting', E_ALL);  ini_set('display_errors', 1);  ini_set('display_startup_errors', 1);    // Параметры соединения с базой данных  $db_server = 'localhost';  $db_name = '*****';  $db_user = '*****';  $db_passwd = '*****';    // Вычисление разницы дат  function dateDifference($date_1 , $date_2 , $differenceFormat = '%a' )  {      $datetime1 = date_create($date_1);      $datetime2 = date_create($date_2);         $interval = date_diff($datetime1, $datetime2);         return $interval->format($differenceFormat);     }    // Подключение к базе данных  $db_connection = @mysql_connect($db_server, $db_user, $db_passwd);  if (!$db_connection || !@mysql_select_db($db_name, $db_connection))  {  	echo "Error during SQL connection";  }  mysql_query("set names utf8");    // Если запрошены текущие данные  if(isset($_GET['last'])) {  	$result = mysql_query("  			SELECT DATE_FORMAT(DATE_ADD(datatime, INTERVAL 1 HOUR),'%d.%m.%Y %H:%i:%s') datatime,t1 t3,t2 t2,t3 t4,t4 t1 FROM temperatures   			ORDER BY datatime DESC  			LIMIT 1");  	$data = [];  	while($row = mysql_fetch_assoc($result)) {	  			$data[] = $row;  	}  	// Выводим только их и заканчиваем на этом  	die(json_encode($data ));  }    $intervalMinutes = 30;  $datastart = date_add(date_create(date('Y-m-d H:i:s')), date_interval_create_from_date_string('-1 days'));  $dataend = date_add(date_create(date('Y-m-d H:i:s')), date_interval_create_from_date_string('1 hour'));  if(isset($_GET['intervalMinutes']))  	$intervalMinutes = intval($_GET['intervalMinutes']);    // Если слишком большой временной промежуток, то не будем наружать сервер большим объемом данных, округлим побольше  $difHours = date_diff($datastart, $dataend);  if($difHours->format('%days')>3)   	$intervalMinutes = 60;  if($difHours->format('%days')>10)   	$intervalMinutes = 180;  if(isset($_GET['datastart']) && $_GET['datastart']!="")  	$datastart = date_create($_GET['datastart']);  if(isset($_GET['dataend']) && $_GET['dataend']!="")  	$dataend = date_create($_GET['dataend']);  	      $result = mysql_query("  	SELECT roundeddatetime datatime, AVG(t1) t3,AVG(t2) t2,AVG(t3) t4,AVG(t4) t1   	FROM  	(  		SELECT DATE_FORMAT(DATE_ADD(DATE_ADD(datatime, INTERVAL (".$intervalMinutes."-MINUTE(datatime)%".$intervalMinutes.") MINUTE), INTERVAL 1 HOUR),'%Y-%m-%d %H:%i:00') roundeddatetime,t1,t2,t3,t4 FROM temperatures   		WHERE datatime>DATE_ADD('".$datastart->format('Y-m-d H:i:s')."', INTERVAL -1 HOUR) AND datatime<DATE_ADD('".$dataend->format('Y-m-d H:i:s')."', INTERVAL -1 HOUR)  	) t1  	GROUP BY roundeddatetime  	LIMIT 3440");  $data = [];  while($row = mysql_fetch_assoc($result)) {	  	$data[] = $row;  }    echo json_encode($data );  ?>

tele.php
<?  // сюда нужно вписать токен вашего бота  define('TELEGRAM_TOKEN', '*****:********');    // сюда нужно вписать ваш внутренний айдишник чата  define('TELEGRAM_CHATID', '414951********599');    //echo message_to_telegram('Данные!');    function message_to_telegram($text) {      $ch = curl_init();  	$url = "https://api.telegram.org/bot".TELEGRAM_TOKEN."/sendMessage?chat_id=".TELEGRAM_CHATID."&text=".$text; // где XXXXX - ваши значения    	$prxy       = '162.33.68.41:56505'; // адрес:порт прокси http://spys.one/proxys/US/  	$prxy_auth = 'auth_user:auth_pass';       // логин:пароль для аутентификации  	curl_setopt_array ($ch, array(CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true));   /********************* Код для подключения к прокси *********************/          curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);  // тип прокси       curl_setopt($ch, CURLOPT_PROXY,  $prxy);                 // ip, port прокси      curl_setopt($ch, CURLOPT_PROXYUSERPWD, $prxy_auth);  // авторизация на прокси      curl_setopt($ch, CURLOPT_HEADER, false);                // отключение передачи заголовков в запросе       curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);            // возврат результата в качестве строки      curl_setopt($ch, CURLOPT_POST, 1);                      // использование простого HTTP POST      curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);        // отмена проверки сертификата удаленным сервером  	curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);  	// Параметры ниже подходят, если у вас PHP свежей версии  	//curl_setopt($ch, CURLOPT_RESOLVE, array("api.telegram.org:443:146.185.158.29"));      //curl_setopt($ch, CURLOPT_DNS_SERVERS, '8.8.8.8,8.8.4.4');  // тип прокси   /***********************************************************************/      $response = curl_exec($ch);  	if ($response === FALSE) {	   		$response = "cURL Error: " . curl_error($ch);	   	}  	$info = curl_getinfo($ch);   	//$response .= '  Took ' . $info['total_time'] . ' seconds for url ' . $info['url']."  ";  	curl_close($ch);   	return $response;  }  ?>
style.css
.current {  	margin-top: 10px;  	padding: 0 10px;  	font-size: 14px;  }  .current div {  	height: 100px;  }  .current b {  	display:inline-block;  	border: 3px solid #ffc700;  	padding: 5px;  	font-size: 30px;  }  .t2 b {  	border: 3px solid #3bd100;  }  .t3 b {  	border: 3px solid #ff0000;  }  .t4 b {  	border: 3px solid #ee00ff;  }  .current .time {  	padding-top: 20px;  	font-weight: bold;  	text-align: center;  	font-size: 18px;  }  .selectperiod {  	padding: 5px 30px;  }  .selectperiod div{  	text-align:center;  }

В архиве лежат еще файлики библиотеки jquery, moment.js и jquery.datetimepicker.
Для красивого отображения графика используется ChartJS.

Итак, теперь у нас сохраняются температуры в базу и даже выводятся в виде красивого графика. Дело за малым — настроить оповещения в Telegram, когда температуры достигают слишком малых значений. По этой инструкции легко настроить бот, который будет присылать вам сообщения, если послать запрос на api.telegram.org. Мой код такой обработки температур и отправки в телеграм в файле index.php:

// Если температуры ниже минимума, отправим сообщение в Telegram  if($t4<40 || $t2<35 || $t1<50 || $t3<50) {  	include 'tele.php';  	message_to_telegram("ГВС_П: $t4, ГВС_О: $t2, Котельная_П: $t1, Котельная_О: $t3");  }

Но вот незадача, телеграм то в России заблокирован! И у большинства провайдеров вы даже IP-адрес сервера не получите по этому имени домена.
Для работы с этим сервером придется использовать SOCKS-прокси (выше есть код файла tele.php), плюс в некоторых случаях потребуется прописать

curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

чтобы запросы начали ходить бесперебойно.

Можно раскомментировать строчку 9 в файле tele.php и проверить отправку, обновив страницу.

Подготовка завершена, осталось разместить все это на местах:

Придерживаясь колхоз-стайла, датчики к трубе можно примотать великим Скотчем, но для лучшей теплопередачи предварительно смазать место контакта термопастой:

Для потомков и случайно оказавшихся в бойлерной слесарей желательно обозначить на корпусе, что это за фиговина с проводами:

Прокинули витую пару с интернетом до бойлерной, подключили роутер, вставили в розетку наше устройство и наслаждаемся картинкой:

Открывать ссылку можно с любого устройства, даже со слабым интернетом. Вообще говоря, веб-сервер можно поднять и на самой ESP8266 и рисовать аналогичный график, заморочившись с обновлением IP-адреса через DynDNS, но «я художник, я так вижу» :) Да и стабильность у такого варианта все-таки куда выше, а еще доработки в серверную часть можно вносить прямо с рабочего места в другом городе, а не программировать каждый раз плату.

На этом можно остановиться, ибо все работает, но есть и пока не решенные проблемы.
Первая — это датчики почему-то иногда сбоят. С периодичностью раз в 4 часа плата почему-то путается в показаниях и присылает откровенную фигню. Один раз даже показало температуру в -70 градусов на одном из датчиков, после чего я внес в серверный код условие добавления в базу значений, только если они все положительные.

Вот пример полученных корявых данных

Второе — если температура все-таки опустится ниже установленного в коде минимума, Telegram-бот вас задолбает сообщениями. Нужно сохранять время предыдущей отправки (например, в файле на хостинге или в базе) и анализировать, прошел ли час (к примеру).

Третье — найденный в интернете прокси-сервер может просто-напросто прекратить свое существование, его нужно будет сменить. А сообщения то вам не будут присылаться в телеграм… Поэтому надо добавить либо оповещение на email, либо сделать парсинг свежих прокси каждый раз. А может кто-то знает лучшее решение?

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

Добавить в избранное +3 +3

(c) 2017 Источник материала

Рекламные ссылки