Автоматизация снятия показаний со счетчиков воды

в 20:02, , рубрики: arduino, mysql, perl, счетчик воды, умный дом

Все знают, что лень двигатель прогресса. Так случилось и в моем случае.

Автоматизация снятия показаний со счетчиков воды

В квартире присутствует 6 точек раздачи воды (3 холодные и 3 горячие). На каждой из точек стоит счетчик.
Каждые 2 счетчика спрятаны за люками скрытого монтажа, один из люков находится за зеркалом, которое нужно снять, чтобы до него добраться.

Вот пара люков:

Автоматизация снятия показаний со счетчиков воды

Раз в месяц с 20 по 25 число необходимо снимать показания со всех счетчиков и отправлять данные в Управляющую Компанию на бланке определенного образца.

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

Сначала перерыл интернет на предмет существующих устройств автоматизации. Нашел только один для меня подходящий — Счетчик импульсов-регистратор «Пульсар» 6-ти канальный. Надо сказать, что стоит он почти 6000 рублей! На самом деле в розницу нигде я его не видел, так как слишком специфический продукт и предполагается, что закупать их будут ТСЖ на все квартиры в доме. Попытался его заказать через интернет в разных местах, но каждый раз, как только доходило до доставки, продавец пропадал. Как я понял, они не любят работать с «физиками», либо был не слишком настойчив.
Ну, нет, так нет — сделаем сами, да еще и дешевле.

Тут то и пригодилась Arduino Mega 2580 с Ethernet модулем, которая была когда-то куплена для различных экспериментов.

Когда делали ремонт в квартире, от каждой точки, где имеются счетчики, до щитка на лестничной клетке, были проложены кабели типа UTP cat 5e. Это было одно из требований контролирующей организации, чтобы в будущем снимать все показания централизованно. Будущее все никак не наступает, а провода пригодились.

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

В итоге, что мы имеем:

  • Счетчики воды
  • Arduino Mega 2580
  • Arduino Ethernet 3.0
  • Бокс для Arduino
  • Блок питания
  • Шлейф для протягивания из слаботочного щитка в шкаф к Arduino.
  • Домашний сервер на Debian с Lighttpd и Mysql

Сами счетчики такие:

Автоматизация снятия показаний со счетчиков воды

Экспериментальным путем было определено, что счетчики работают не просто, а очень просто. Когда последний разряд меняет свое значение с 9 на 0, замыкается геркон внутри счетчика. В таком состоянии он находится до того, пока значение последнего разряда не станет равным 3. Т.е. фактически нам надо фиксировать момент перехода из состояния «разомкнуто» в состояние «замкнуто». Заострю внимание, что мы фиксируем ТОЛЬКО факт перехода из одного состояния в другое, потому что система может обесточиться, да и вообще, мало ли какие могут быть коллизии.

В момент замыкания геркона, Arduino по HTTP вызывает простенький perl-скрипт на сервере, где крутится lighttpd. Скрипт записывает в базу данных этот момент. Другой скрипт позволяет смотреть текущее состояние счетчиков.

Скетч Arduino с комментариями:

#include <Ethernet.h>
#include <SPI.h>
#include <Bounce2.h> // Эту библиотеку необходимо скачать тут: https://github.com/thomasfredericks/Bounce-Arduino-Wiring

byte mac[] = {0x90,0xA2,0xDA,0x0E,0xF1,0x92}; // MAC-адрес нашего устройства (написан на наклейке платы Ethernet shield)
IPAddress ip(192,168,1,11); // IP адрес, если вдруг не получится получить его через DHCP
//IPAddress server(192,168,1,10); // ip-адрес удалённого сервера (использовался, пока не было имени)
char server[] = "smarthome.mydomain.ru"; // Имя удалённого сервера
char request[40]; // Переменная для формирования ссылок
int CounterPin[6] = {22,23,24,25,26,27}; // Объявляем массив пинов, на которых висят счетчики
char *CounterName[6] = {"0300181","0293594","0300125","0295451","0301008","0293848"}; // Объявляем массив имен счетчиков, которые мы будем передавать на сервер
Bounce CounterBouncer[6] = {}; // Формируем для счетчиков Bounce объекты
EthernetClient rclient; // Объект для соединения с сервером

void setup() {
  //Serial.begin(9600);
  for (int i=0; i<6; i++) {
    pinMode(CounterPin[i], INPUT); // Инициализируем пин
    digitalWrite(CounterPin[i], HIGH); // Включаем подтягивающий резистор
    CounterBouncer[i].attach(CounterPin[i]); // Настраиваем Bouncer
    CounterBouncer[i].interval(10); // и прописываем ему интервал дребезга
  }
  // Инициализируем сеть
  if (Ethernet.begin(mac) == 0) {
    Ethernet.begin(mac, ip); // Если не получилось подключиться по DHCP, пробуем еще раз с явно указанным IP адресом
  }
  delay(1000); // даем время для инициализации Ethernet shield
}

void loop() {
  delay(1000); // Задержка в 1 сек, пусть будет :)
  // Проверяем состояние всех счетчиков
  for (int i=0; i<6; i++) {
    boolean changed = CounterBouncer[i].update();
    if ( changed ) {
      int value = CounterBouncer[i].read();
      // Если значение датчика стало ЗАМКНУТО
      if ( value == LOW) {
        //Serial.println(CounterPin[i]);
        sprintf(request, "GET /input.pl?object=%s HTTP/1.0", CounterName[i]); // Формируем ссылку запроса, куда вставляем имя счетчика
        sendHTTPRequest(); // Отправляем HTTP запрос
      }
    }
  }
}

// Функция отправки HTTP-запроса на сервер
void sendHTTPRequest() {
  if (rclient.connect(server,80)) {
    rclient.println(request);
    rclient.print("Host: ");
    rclient.println(server);
    rclient.println("Authorization: Basic UmI9dlPnaJI2S0f="); // Base64 строка, полученная со значения "user:password"
    rclient.println("User-Agent: Arduino Sketch/1.0");
    rclient.println();   
    rclient.stop();
  }
}

На сервере крутится: Debian, Lighttpd, Mysql. В свою очередь на нем имеется два perl-скрипта: один для записи состояний счетчиков в базу, второй для вывода текущих показаний.

input.pl

#!/usr/bin/perl -w

use strict;
use CGI::Fast;
use DBI;

while(my $q = CGI::Fast->new)
{
    main($q);
}

sub main
{
    my $q = shift;
    my $dbh = DBI->connect( "dbi:mysql:database=smart_home;mysql_client_found_rows=1;mysql_enable_utf8=1;mysql_socket=/var/run/mysqld/mysqld.sock", 'dbname', 'password',
    {
        RaiseError => 1,
        AutoCommit => 1,
        mysql_multi_statements => 1,
        mysql_init_command => q{SET NAMES 'utf8';SET CHARACTER SET 'utf8'}
    } ) or die "Cannot connect";
    $dbh->{mysql_auto_reconnect} = 1;
    print "Content-Type: text/html; charset=UTF-8nn";
    print "OKn";
    my $object = $q->param("object");
    if ($object)
    {
        $dbh->do(q{INSERT INTO water_count (object) VALUES(?)},undef,$object) or die $dbh->errstr;
    }
}

result.pl

#!/usr/bin/perl -w

use strict;
use CGI::Fast;
use DBI;

# массив стартовых показаний счетчиков
my $start = {
    "0300125" => 102.53,
    "0301008" => 75.31,
    "0300181" => 65.92,
    "0293594" => 54.51,
    "0293848" => 55.04,
    "0295451" => 87.43
};

while(my $q = CGI::Fast->new)
{
    main($q);
}

sub main
{
    my $dbh = DBI->connect( "dbi:mysql:database=smart_home;mysql_client_found_rows=1;mysql_enable_utf8=1;mysql_socket=/var/run/mysqld/mysqld.sock", 'dbname', 'password',
    {
        RaiseError => 1,
        AutoCommit => 1,
        mysql_multi_statements => 1,
        mysql_init_command => q{SET NAMES 'utf8';SET CHARACTER SET 'utf8'}
    } ) or die "Cannot connect";
    $dbh->{mysql_auto_reconnect} = 1;
    print "Content-Type: text/html; charset=UTF-8nn";
    print "Текущие показания счетчиков:<br>";
    my $sql = "SELECT count(*) as c,object FROM water_count group by object";
    my $sth = $dbh->prepare($sql);
    $sth->execute;
    while (my ($count, $object) = $sth->fetchrow_array())
    {
        $start->{$object} = sprintf("%.2f",$start->{$object}+$count/100);
    }
    $sth->finish;
    foreach my $object (keys $start) {
        my ($intcurrent,$fine) = split(/./,$start->{$object});
        print "$object <b>$intcurrent</b>.$fine<br>n";
    }
}

Mysql база с одной таблицей:

CREATE TABLE `water_count` (
  `object` varchar(20) NOT NULL DEFAULT '',
  `datetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8

В таблице есть только два поля. Первое — название объекта (в нашем случае это номер счетчика). Второе — дата и время в формате TIMESTAMP, которые заполняются автоматически, когда происходит вставка строки.

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

Что дальше?
Дальше хочется ежемесячную автоматическую распечатку на заполненном бланке.
Так же хочется подключить счетчик электроэнергии с передачей данных в Мосэнергосбыт, а потом и с их оплатой.
Статистика, графики и прочие радости работы с данными.

Автор: BlackRaincoat

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js