Pull to refresh

Comments 37

Спасибо за полезную информацию! Счетчики массовые и так подключать и считывать данные с них правда легче
Руководство для тех, кто не вкуривает официальную вики проекта.
Но сам факт статьи радует.
… либо можно купить RS-232 <-> LAN шлюз и использовать облачный сервис яЭнергетик. Он там чего-то стоит, но какие-то десятки рублей в месяц, кажется.
в меркуриях нет RS-232, там CAN и RS-485
несущественно ;)
image
Могу попросить Романа, чтобы прочитал статью — может вышлет свои замечания :-)
Критики от Романа мне и на форуме хватало, хотя конечно пусть прочтет, почему бы и нет.
Да Роман нетерпим к другим зачастую :-)
Хотя и «голова», что есть, то есть.
Не плохо!
Тоже подумываю к своему счетчику подключится, только у меня Микрон, но протокол похож.
Статью, как писал модуль сбора данных для Fastwel IO, писать?
Опязательно пишите) Сравню ваш опыт со своим)
конечно писать…
UFO landed and left these words here
Нет, на линуксе СКАДА крутится, которая и собирает данные со счетчика. А что крутится в самом меркурии понятия не имею.
Вовсе это не единственное бесплатное решение. Вот, например здесь

За статью спасибо.
У нас есть своя система, в частности в ней одна из задач как раз энергоучёт. Закупать или ставить стороннее приложение не хотели по внутренним причинам, поэтому реализовали на php протокол опроса Меркуриев 230/234. В итоге около 60 счётчиков подключены группами по 7-10шт к преобразователям в Ethernet (Moxa) и на FreeBSD скрипт раз в 5 минут снимает актуальные показания или, если был пропущен какой-то часовой интервал, считывает их из памяти Меркурия. Если интересно, поделюсь кодом.
В php я не шарю, но думаю другим пригодится, было бы неплохо написать об этом статью и где то выложить код.
Код php использует официальный протокол Меркуриев и идеи от дядьки, код которого очень помог разобраться в протоколе. В моём примере пароль админа в счётчике заменён на стандартный. По умолчанию адрес счётчика — это последние 2 или 3 цифры в серийном номере. Каждая moxa и её порт опрашиваются параллельно. Запрашиваем по одному все счётчики, висящие на одном порту moxa.
Алгоритм (каждые 5 минут часа):
— Читаем серийный номер счётчика, для контроля;
— Читаем онлайн данные по фазам: силу тока, мощность, напряжение, косинус фи;
— Пишем в БД;
— Если первые 4 минуты любого часа дня, то считываем общее потребление за прошедший час;
— Если между 5-ю и 10-ю минутами 0-го часа, то проверяем все ли показания есть за предыдущий период в БД. Если что-то отсутствует, то считываем показания из памяти счётчика;

P.S. То, что данные читаются онлайн, не из памяти счётчиков и могут браться не точно с 0-минут до 0-минут следующего часа, а с дельтой в несколько секунд не является принципиальным моментом, т.к. эта ж дельта будет и в следующем часу.

Собственно сам код
#!/usr/local/bin/php
<?php 
/**
 * \file Считываем показания со счётчиков
 */
include_once realpath(dirname($_SERVER['SCRIPT_FILENAME'])).'/general.php';
include_once ABSOLUTE_CLASS.'db.php';

class eCounters {
    // дескриптор открытого канала на моху
    private $fp = 0;

    // 0 - слать на email И ЗАПИСЫВАТЬ в базу. 1(потом пид) - отладка на экран и НЕ записывать в базу.
    public $debug = 0;
    public $db = true;

    // Строка ошибки
    public $errMsg = '';

    // Параметры соединения
    private $ip, $port, $dev;

    // Параметры merc_gd
    private $t3byte = 1;
    private $t4byte = 2;
    private $tSerial = 3;
    private $tNormal = 4;

    // Версия прошивки
    private $ver;
    
    // http://ab-log.ru/smart-house/mercury-230
    function CRC_Modbus($val='') {
        $Data=hex2bin($val);
        $len=strlen($Data);
        $Sum=0xFFFF;
        $cou = 0;
        while ($len--){
            $Sum^= ord($Data[$cou]);
            for ($shift_cnt=0; $shift_cnt<8; $shift_cnt++) {
                if (($Sum&0x1)==1) $Sum=(($Sum>>1)^0xA001);
                else $Sum>>=1;
            }
            $cou++;
        }
        $Sum=dechex($Sum);
        $len=4-strlen($Sum);
        while ($len--){
            $Sum='0'.$Sum;
        }
        return $val.$Sum[2].$Sum[3].$Sum[0].$Sum[1];
    }

    // Добавляет 0 к началу числа до $lim длинны
    function zeropad($num, $lim) {
        return (strlen($num) >= $lim) ? $num : $this->zeropad('0'.$num, $lim);
    }

    // Переводим бинарные динные в текстовое 16-тиричное представление
    function dd($data = '') {
        $result = '';
        $data2 = '';
        for ( $j = 0; $j < count($data); $j++ ) {
            $data2 = dechex(ord($data[$j]));
            if ( strlen($data2) == 1  )
                $result = '0'.$data2;
            else
                $result .= $data2;
        }

        return $result;
    }

    function _hexdec($hex_string) {
        $result=hexdec($hex_string);

        if ( $result <= 9 )
            return '0'.$result;
        else
            return $result;
    }

    /*
     * Отправляет на счётчик команду, читает и декодирует ответ
     * $cmd - команда а отправку
     * $resp_len - ожидаемая длинна ответа
     * $factor - коэффициент, на который надо умножить результат, т.к. результат чтения из потока только целочисленный
     * $total - тип ответа, в зависимости от этого отдаётся разное представление данных
     * $recurse_in - сам себя вызывает в случае неудачной попытки работы с потоком
     */
    function merc_gd($cmd, $resp_len, $factor = 1, $total = 0, $recurse_in = 0) {

        if($this->debug >= 3)
            echo $this->dev.': cmd:'.$cmd."\n";

        if($total == 0)
            $total = $this->t3byte;
            
        // 3 попытки на выполнение команды
        if($recurse_in)
            $retry=0;
        else
            $retry=3;
        do {
            flush();
            fwrite($this->fp, hex2bin($cmd));
            // 6 попыток прочитать ответ
            $read_retry=6;
            $result='';
            do {
                $read_retry--;
                if (false === ($result .= stream_get_contents($this->fp)))
                    $read_retry=0;
            } while (strlen($result)!=$resp_len && $read_retry );
            if ( !$read_retry || (bin2hex($result) != $this->CRC_Modbus(substr(bin2hex($result),0,strlen(bin2hex($result))-4)))) {
                $this->close_connection();
                if(!$retry--) {
                    // Почтой, что счётчик недоступен
                    if ($this->debug >= 1)
                        echo $this->dev.': Moxa error: Cant recive data from: '.$this->ip.':'.$this->port.' dev:'.$this->dev."\n";
                    else
                        $this->errMsg .= 'Cant recive data from: '.$this->ip.':'.$this->port.' dev:'.$this->dev."\r\n<br>";
                    return false;
                }
                if (false === $this->init_connection())
                    return false;
            }
            else
                break;
        } while (1);
    
        $ret = array();
        $start_byte = 1;
	
        if ( $total == $this->t3byte )
        {
            // 3-х байтовый ответ по текущим показателям
            for ( $i = 0; $i < 4; $i++ )
            {
                $mask=63; // 3Fh - надо убрать 2 старших бита, это направление энергии
                $sign_mask=128; // 80h - направление течения
                if ( (ord($result[$start_byte + $i * 3]) & $sign_mask) > 0 )
                    $sign=-1;
                else
                    $sign=1;
                if ( strlen($result) > $start_byte + 2 + $i * 3 )
                    $ret[$i] = hexdec($this->dd($result[$start_byte + $i * 3] & $mask).
                                    $this->dd($result[$start_byte + $i * 3 + 2]).
                                    $this->dd($result[$start_byte + $i * 3 + 1]))*$factor*$sign;
            }
        }
        elseif ( $total == $this->t4byte )
            // 4-х байтовый ответ
            $ret[0] = hexdec($this->dd($result[$start_byte+1]).
                            $this->dd($result[$start_byte]).
                            $this->dd($result[$start_byte+3]).
                            $this->dd($result[$start_byte+2]))*$factor;
        elseif ( $total == $this->tSerial )
            // Тут запрос серийника
            $ret[0] = $this->_hexdec($this->dd($result[$start_byte])).
                    $this->_hexdec($this->dd($result[$start_byte+1])).
                    $this->_hexdec($this->dd($result[$start_byte+2])).
                    $this->_hexdec($this->dd($result[$start_byte+3]));
        elseif ( $total == $this->tNormal )
            // Просто отдать строку
            $ret[0] = bin2hex($result);

        if($this->debug >= 3)
            echo $this->dev.': ret:'.$ret[0]."\n";
        return $ret;
    }

    function init_connection()
    {
        $retry=3;
        do
        {
            $retry--;
            sleep(1);
        } while ( $retry && (false === ($this->fp = fsockopen($this->ip, $this->port, $errno, $errstr, 10))) );
        if (! $retry)
        {
            //Попробуем перезапустить моху
            if (false === file_get_contents('http://'.$this->ip.'/09Set.htm?Submit=Submit'))
            {
                // Почтой, что моха недоступна
                if ($this->debug >= 1)
                    echo $this->dev.': Moxa unavailable: Can not connect to: '.$this->ip."\n";
                else
                    $this->errMsg .= 'Moxa unavailable: Can not connect to: '.$this->ip."\r\n";
                return false;
            }
            sleep(5);
            $retry=3;
            do
            {
                $retry--;
                sleep(1);
            } while ( $retry && (false === ($this->fp = fsockopen($this->ip, $this->port, $errno, $errstr, 10))) );
            if (! $retry)
            {
                // Почтой, что моха недоступна
                if ($this->debug >= 1)
                {
                    echo $this->dev.': Moxa unavailable: Restart done, but can not connect to serial port to: '.$this->ip.':'.$this->port."\n";
                    echo $this->dev.": Moxa: $errstr ($errno)\n";
                }
                else
                    $this->errMsg .= 'Moxa unavailable: Restart done, but can not connect to serial port to: '.$this->ip.':'.$this->port."\r\nMoxa: $errstr ($errno)\r\n";
                return false;
            }
        }

        // около 5 мс стандартная длительность тайм-аута для скорости 9600 Бод
        // но 30 милисекунд ещё не успевает, с запасом 50
        stream_set_timeout($this->fp, 0, 500000);

        // Инициализация соединения и передача пароля
        // Последний аргумент - рекурсия = 1, чтобы был только 1 заход без рекурсии, иначе вызовет сам себя и...
        if (false === ($init = $this->merc_gd($this->CRC_Modbus($this->dev.'0101010101010101'), 4, 1, $this->tNormal, 1)))
            return false;
        if ($this->debug >= 1)
            echo $this->dev.': Init:'.$init[0]."\n";

        // Проверим версию счётчика, если базовый № прошивки >= 9, то новые счётчики
        if (false === ($ret = $this->merc_gd($this->CRC_Modbus($this->dev.'0803'), 6, 1, $this->tNormal, 1)))
            return false;
        if ($this->debug >= 1)
            echo $this->dev.': Version:'.substr($ret[0], 2, 2).'.'.substr($ret[0], 4, 2).'.'.substr($ret[0], 6, 2)."\n";
        $this->ver = substr($ret[0], 2, 2);
        
        return true;
    }

    function close_connection()
    {
        if(!$this->fp)
            return;
        flush();
        fwrite($this->fp, hex2bin($this->CRC_Modbus($this->dev.'02')));
        stream_get_contents($this->fp);
        sleep(1);
        fclose($this->fp);
        $this->fp=0;
    }

    // $only_total - 0 или не 0, т.е. только итоговое значение, без текущих
    function get_from_nport_inner($only_total)
    {

        if (false === $this->init_connection())
            return false;

        // Серийный номер
        if (false === ($serial = $this->merc_gd($this->CRC_Modbus($this->dev.'0800'), 10, 1, $this->tSerial)))
            return false;
        if ($this->debug >= 1)
            echo $this->dev.': Serial:'.$serial[0]."\n";

        if (!$only_total)
        {
            // Сила тока (А) по фазам
            if (false === ($Ia = $this->merc_gd($this->CRC_Modbus($this->dev.'081621'), 12, 0.001)))
               return false;
        if ($this->debug >= 1)
            echo $this->dev.": Ia: $Ia[0] + $Ia[1] + $Ia[2]\n";

            // Мощность P (Вт) по фазам
            $retry=3;
            $done=0;
            do {
                if (false === ($Pv = $this->merc_gd($this->CRC_Modbus($this->dev.'081600'), 15, 0.01)))
                    return false;
                if (round($Pv[0], 2) == round($Pv[1] + $Pv[2] + $Pv[3], 2))
                {
                    $done=1;
                    $error = '';
                }
                else
                {
                    if ( !$retry-- )
                        $done=1;
                    $error = "error, ".round($Pv[1] + $Pv[2] + $Pv[3], 2);
                }
                if ($this->debug >= 1)
                    echo $this->dev.": Pv: $Pv[0] = $Pv[1] + $Pv[2] + $Pv[3] $error\n";
            } while( !$done );
    
            // Напряжение U (В) по фазам
            if (false === ($Uv = $this->merc_gd($this->CRC_Modbus($this->dev.'081611'), 12, 0.01)))
                return false;
            if ($this->debug >= 1)
                echo $this->dev.": Uv: $Uv[0] + $Uv[1] + $Uv[2]\n";

            // Коэффициент мощности (С) по фазам
            $retry=3;
            $done=0;
            do {
                if (false === ($Cos = $this->merc_gd($this->CRC_Modbus($this->dev.'081630'), 15, 0.001)))
                    return false;
                if (round($Cos[0], 1) == round(($Cos[1] + $Cos[2] + $Cos[3])/3, 1))
                {
                    $done=1;
                    $error = '';
                }
                else
                {
                    if ( !$retry-- )
                        $done=1;
                    $error = "error, ".round(($Cos[1] + $Cos[2] + $Cos[3])/3, 1);
                }
                if ($this->debug >= 1)
                    echo $this->dev.": Cos: $Cos[0]= ($Cos[1] + $Cos[2] + $Cos[3])/3 $error\n";
            } while( !$done );
            for ( $j = 1; $j < 4; $j++ ) {
                if ($Cos[$j] > 1)
                    $Cos[$j]=1;
            }

            if ($this->db)
            {
                $pgsql = new db('pgsql','');
                $res=$pgsql->select('SELECT eid FROM electric_accounting_counters WHERE model_id=\''.$serial[0].'\'', 'num');
                if($pgsql->error[0]!=00000){echo $pgsql->error[2];}
                if (!isset($res[0][0]))
                    return false;
                $eid=$res[0][0];
                if($this->debug >= 1)
                    echo $this->dev.': eid='.$eid."\n";
                $sql=<<<TEXT
                    INSERT INTO electric_accounting_piucos 
                    VALUES (DEFAULT, $eid, 
                        $Pv[1], $Pv[2], $Pv[3],
                        $Ia[0], $Ia[1], $Ia[2],
                        $Uv[0], $Uv[1], $Uv[2],
                        $Cos[0], $Cos[1], $Cos[2]);
TEXT;
                $pgsql->set($sql);
            }
        }
    
        $timea=date('Y-m-d H:00:00');
        $timeb=date('Y-m-d H:i:s');
        // Если первые 4 минуты часа
        if (strtotime($timeb) - strtotime($timea) < 240 || $this->debug >= 1)
        {
            // Общее потребление
            if (false === ($Tot = $this->merc_gd($this->CRC_Modbus($this->dev.'050000'), 19, 0.001, $this->t4byte)))
                return false;
            if ($this->debug >= 1)
                echo $this->dev.": Total: $Tot[0]\n";
            if (!isset($pgsql))
            {
                $pgsql = new db('pgsql','');
                $res=$pgsql->select('SELECT eid FROM electric_accounting_counters WHERE model_id=\''.$serial[0].'\'', 'num');
                if($pgsql->error[0]!=00000){echo $pgsql->error[2];}
                if (!isset($res[0][0]))
                    return false;
                $eid=$res[0][0];
                if($this->debug >= 1)
                    echo $this->dev.': eid='.$eid."\n";
            }
            $res=$pgsql->select('SELECT total FROM electric_accounting_last_value WHERE eid='.$eid, 'num');
            if (!isset($res[0][0])) {
                if($this->debug >= 1)
                    echo $this->dev.': Last in DB=(none)'."\n";
                if($this->debug >= 2)
                    echo $this->dev.": Insert first time 'last value'\n";
                if ($this->db)
                    $pgsql->set('INSERT INTO electric_accounting_last_value VALUES ( DEFAULT, '.$eid.', '.round($Tot[0],3).' )');
            }
            else
            {
                if($this->debug >= 1)
                    echo $this->dev.': Last in DB='.$res[0][0]."\n";
                if($this->debug >= 2) {
                    echo $this->dev.": Insert energy\n";
                    echo $this->dev.': Save energy to DB(pre) Tot:'.round($Tot[0],3).' last:'.round($res[0][0],3)."\n";
                    echo $this->dev.': Save energy to DB='.round(round($Tot[0],3)-round($res[0][0],3),3)."\n";
                }
                if ($this->db) {
                    $pgsql->set('UPDATE electric_accounting_last_value SET total='.round($Tot[0],3).' WHERE eid='.$eid);
                    $pgsql->set('INSERT INTO electric_accounting_energy VALUES ( DEFAULT, '.$eid.', '.round(round($Tot[0],3)-round($res[0][0],3),3).' )');
                }
            }
        }

        $timea=date('Y-m-d 00:05:00');
//        $timea=date('Y-m-d H:05:00');
        $timeb=date('Y-m-d H:i:s');
        // Если между 5-ю минутами и 10-ю 0-го часа каждого дня
        if ((strtotime($timeb) - strtotime($timea) < 240 && strtotime($timeb) > strtotime($timea)) || $this->debug >= 1)
        {
            // проверяем за последние 2 месяца
            if (!isset($pgsql))
            {
                $pgsql = new db('pgsql','');
                $res=$pgsql->select('SELECT eid FROM electric_accounting_counters WHERE model_id=\''.$serial[0].'\'', 'num');
                if($pgsql->error[0]!=00000){echo $pgsql->error[2];}
                if (!isset($res[0][0])) {
                    if ($this->debug >= 1) {
                        $this->close_connection();
                        return true;
                    }
                    else
                        return false;
                }
                $eid=$res[0][0];
                if($this->debug >= 1)
                    echo $this->dev.': eid='.$eid."\n";
            }
            $timeb=date('Y-m-d');
//            $timea=date('Y-m-d',strtotime('now - 2 month'));
//            $timea=date('Y-m-d',strtotime('now - 3 week'));
            $timea=date('Y-m-d',strtotime('now - 2 week'));
//            $timea=date('Y-m-d',strtotime('now - 2 day'));
            $sql=<<<SQL
                WITH tt AS (
                    SELECT generate_series('{$timea}'::timestamp, '{$timeb}', '1 hour') AS dt) 
                SELECT * FROM tt 
                WHERE tt.dt NOT IN (
                    SELECT indication_date 
                    FROM electric_accounting_energy
                    WHERE indication_date BETWEEN '{$timea}' AND '{$timeb}' 
                    AND eid={$eid});
SQL;
            $res=$pgsql->select($sql, 'num');
            if (!isset($res[0][0])) {
                $this->close_connection();
                return true;
            }

            // получим последнюю записанную ячейку памяти
            if (false === ($ret = $this->merc_gd($this->CRC_Modbus($this->dev.'0813'), 12, 1, $this->tNormal)))
                return false;
            // В разных версиях - разная адресация
            if($this->ver >= 9) {
                $addr = substr($ret[0], 3, 3).'0';
                $h_addr = substr($ret[0], 2, 1);
            }
            else {
                // адрес в памяти
                $addr = substr($ret[0], 2, 4);
                // байт состояния записи, нам нужен только 4-й бит (маской 10h) - это 17-й бит(старший) адреса памяти
                $h_addr = hexdec(substr($ret[0], 6, 2)) & 16 / 16;
            }
            if($this->debug >= 3) {
                echo $this->dev.': addr='.$addr."\n";
                echo $this->dev.': h_addr='.$h_addr."\n";
            }
            // время и дата
            $hh = substr($ret[0], 8, 2); $mm = substr($ret[0], 10, 2);
            $dd = substr($ret[0], 12, 2); $mo = substr($ret[0], 14, 2); $yy = substr($ret[0], 16, 2);
            $timeb = "$yy-$mo-$dd $hh:$mm:00";
            if($this->debug >= 1)
                echo $this->dev.': Last saved time:'.$timeb."\n";
            // длительность периода интегрирования
            $period = hexdec(substr($ret[0], 18, 2));
            if($this->debug >= 1)
                echo $this->dev.': Integrity period:'.$period."\n";

            // прочитаем вариант исполнения счётчика
            if (false === ($ret = $this->merc_gd($this->CRC_Modbus($this->dev.'0812'), 9, 1, $this->tNormal)))
                return false;
            // проверим есть ли память
            if ((hexdec(substr($ret[0], 4, 2)) & 32) != 32 ) {
                $this->close_connection();
                return true;
            }
            // постоянная счётчика
            switch (hexdec(substr($ret[0], 4, 2)) & 15) {
                case 0:
                    $const = 5000;
                    break;
                case 1:
                    $const = 25000;
                    break;
                case 2:
                    $const = 1250;
                    break;
                case 3:
                    $const = 500;
                    break;
                case 4:
                    $const = 1000;
                    break;
                case 4:
                    $const = 250;
                    break;
                default:
                    if ($this->debug >= 1)
                        echo $this->dev.': Counter error: const error: '.$this->ip.':'.$this->port.' dev:'.$this->dev."\n";
                    else
                        $this->errMsg .= 'Counter error: const error: '.$this->ip.':'.$this->port.' dev:'.$this->dev."\r\n";
                    return true;
            }
            if($this->debug >= 1)
                echo $this->dev.': Counter const:'.$const."\n";
            // сколько памяти (нужен ли 17-й бит)
            $ext_mem = hexdec(substr($ret[0], 8, 2)) & 128 / 128;
            if($this->debug >= 2)
                echo $this->dev.': ext_mem='.$ext_mem."\n";

            // сколько памяти используется под запись мощности
            // если установлен хотя бы 1 бит учёта любого вида технических потерь, то расширенная память не используется
            if ($ext_mem == 1) {
                if (false === ($ret = $this->merc_gd($this->CRC_Modbus($this->dev.'081e'), 5, 1, $this->tNormal)))
                    return false;
                if($this->debug >= 2)
                    echo $this->dev.': check using ext_mem...'."\n";
                if (hexdec(substr($ret[0], 2, 2)) > 0)
                    $ext_mem = 0;
                if($this->debug >= 2)
                    echo $this->dev.': ext_mem='.$ext_mem."\n";
            }

            // Теперь в цикле пройдём по всем датам
            $i = 0;
            $db_date = $res[0][0];
            if($this->debug >= 1)
                echo $this->dev.': Reading memory...'."\n";
            while (1) {
                if($this->debug >= 1)
                    echo $this->dev.': Finding date:'.$db_date."\n";
                // Найдём разницу во времени
                $diff = strtotime($timeb)-strtotime($db_date);
                if($this->debug >= 3)
                    echo $this->dev.': orig diff='.$diff."\n";
                // Сколько периодов между ними
                $diff = $diff / 60 / $period;
                if($this->debug >= 3)
                    echo $this->dev.': priod diff='.$diff."\n";
                // Теперь надо отступить ещё на час минус учтённый последний период,
                // т.к. разница на конец часа(т.е. конец последнего периода)
                $diff = $diff + 60 / $period - 1;
                // Сколько ячеек памяти надо отступить
                $diff *= 16;
                if($this->debug >= 3) {
                    echo $this->dev.': cell diff='.$diff."\n";
                    echo $this->dev.': cell diff(hex)='.$this->zeropad(dechex($diff),4)."\n";
                }
                // Получим показания периодов часа
                $j = 60 / $period;
                $p = 0;
                while( $j > 0 ) {
                    if ($ext_mem) {
                        $addr_a = hexdec($addr)+hexdec('10000')*$h_addr;
                        $addr_b = $addr_a - $diff;
                        if ($addr_b < 0 )
                            $addr_b = hexdec('20000')+$addr_b;
                    }
                    else {
                        $addr_a = hexdec($addr);
                        $addr_b = $addr_a - $diff;
                        if ($addr_b < 0 )
                            $addr_b = hexdec('10000')+$addr_b;
                    }
                    if($this->debug >= 3) {
                        echo $this->dev.': addr_a='.$this->zeropad(dechex($addr_a),4)."\n";
                        echo $this->dev.': addr_b='.$this->zeropad(dechex($addr_b),4)."\n";
                    }
                    if ($addr_b < hexdec('10000'))
                        $bit = '03';
                    else {
                        $bit = '83';
                        $addr_b -= hexdec('10000');
                    }
                    $addr_r = $this->zeropad(dechex($addr_b), 4);
                    if($this->debug >= 3)
                        echo $this->dev.': addr_r='.$addr_r."\n";
            
                    // Считываем показания
                    if (false === ($ret = $this->merc_gd($this->CRC_Modbus($this->dev.'06'.$bit.$addr_r.'0f'), 18, 1, $this->tNormal)))
                        return false;
            
                    // время и дата
                    $hh = substr($ret[0], 4, 2); $mm = substr($ret[0], 6, 2);
                    $dd = substr($ret[0], 8, 2); $mo = substr($ret[0], 10, 2); $yy = substr($ret[0], 12, 2);
                    $timea = "$yy-$mo-$dd $hh:$mm:00";
                    if($this->debug >= 1)
                        echo $this->dev.': Getting date:'.$timea."\n";
                    // проверим та ли дата в ячейке памяти
                    if (abs(strtotime($db_date)-strtotime($timea)) > 3600 ) {
                        if ($this->debug >= 1)
                            echo $this->dev.': Counter error: memory error, to long period ('.$db_date.'): '.$this->ip.':'.$this->port.' dev:'.$this->dev."\n";
                        else
                            $this->errMsg .= 'Counter error: memory error, to long period ('.$db_date.'): '.$this->ip.':'.$this->port.' dev:'.$this->dev."\r\n<br>";
                        $i++;
                        break;
                    }
                    // Показание за период
                    $p += hexdec(substr($ret[0], 18, 2).substr($ret[0], 16, 2)) * (60 / $period) / (2 * $const);
                    $j--;
                    $diff -= 16;
                }
                if($this->debug >= 1)
                    echo $this->dev.': Reading 1 cell done'."\n";
                if ( $j > 0 ) {
                    $i++;
                    if (isset($res[$i][0])) {
                        $db_date = $res[$i][0];
                        continue;
                    }
                    else
                        break;
                }
                $p = $p / ( 60 / $period );

                // Запишем в базу
                if (isset($res[$i][0])) {
                    if ($res[$i][0] == $db_date) {
                        if ($this->debug >= 1)
                            echo $this->dev.': INSERT ( DEFAULT, '.$eid.', '.$p.', \''.$db_date.'\' )'."\n";
                        if ($this->db)
                            $pgsql->set('INSERT INTO electric_accounting_energy VALUES ( DEFAULT, '.$eid.', '.$p.', \''.$db_date.'\' )');
                        $i++;
                        if (isset($res[$i][0])) {
                            if (date('Y-m-d H:i:00',strtotime($db_date.' + 1 hour')) == $res[$i][0]) 
                                $db_date = $res[$i][0];
                            else 
                                $db_date = date('Y-m-d H:i:00',strtotime($db_date.' + 1 hour'));
                        }
                        else 
                            $db_date = date('Y-m-d H:i:00',strtotime($db_date.' + 1 hour'));
                    }
                    else {
                        if ($this->debug >= 1)
                            echo $this->dev.': UPDATE energy='.$p.' WHERE eid='.$eid.' AND indication_date=\''.$db_date.'\''."\n";
                        if ($this->db)
                            $pgsql->set('UPDATE electric_accounting_energy SET energy='.$p.' WHERE eid='.$eid.' AND indication_date=\''.$db_date.'\'');
                        $db_date = $res[$i][0];
                    }
                }
                else {
                    if ($this->debug >= 1)
                        echo $this->dev.': UPDATE energy='.$p.' WHERE eid='.$eid.' AND indication_date=\''.$db_date.'\' )'."\n";
                    if ($this->db)
                        $pgsql->set('UPDATE electric_accounting_energy SET energy='.$p.' WHERE eid='.$eid.' AND indication_date=\''.$db_date.'\'');
                    break;
                }
            }
        }
        // Закрытие соединения
        $this->close_connection();

        return true;
    }

    function get_from_nport_main($only_total=0)
    {
        $retry=3;

        while ( $retry && (false === $this->get_from_nport_inner($only_total)) ) 
        {
            $retry--;
            $this->close_connection();
            sleep(5);
        }
    
        if (! $retry)
        {
            if ($this->debug >= 1)
                echo $this->dev.': Moxa error: Communications error with: '.$this->ip.':'.$this->port."\n";
            else
                $this->errMsg .= 'Moxa error: Communications error with: '.$this->ip.':'.$this->port."\r\n\r\n";
        }
    }

    function get_from_nport($ip, $port, $devs)
    {
        $this->ip = $ip;
        $this->port = $port;
        
        if(!isset($this->debug))
            $this->debug = 0;
            
        foreach($devs as $dev) {
            $this->dev = $dev[0];
            if(!isset($dev[1]))
                $this->get_from_nport_main();
            else
                $this->get_from_nport_main($dev[1]);
        }
    }
}

function shutdown() {
//    ob_end_clean();
        posix_kill(getmypid(), SIGHUP);
}

// ---------------------------------------------------------------------
$cntr = new eCounters;
$cntr->debug = 0;
$executed = 0;
// Debug
//$cntr->debug = 2;
//$cntr->db = false;
//$cntr->get_from_nport('192.168.1.1', 4001, array(['25'],['31'],['3e'],['4e']));
//exit;
// ---------------------------------------------------------------------
// K трансф = K напряжения * K тока
//
// ---------------------------------------------------------------------
// Подстанция
//
// 1. 21(15h)  Ф. Моторная  K=60*400=24000  - 2-й транс.
// 2. 37(25h)  Ф. 157       K=60*400=24000  - 1-й транс
$pid = pcntl_fork();
if($pid == -1)
    // Fork not woking
    $cntr->get_from_nport('192.168.1.1', 4002, array(['15'],['25']));
elseif($pid) 
    // Parent
    $executed++;
else{
    // Child
    register_shutdown_function('shutdown');
    $cntr->get_from_nport('192.168.1.1', 4002, array(['15'],['25']));
    if ($cntr->errMsg != '')
        mail(MAIL_FROM, 'Moxa error', $cntr->errMsg, 'From: moxa@eldin.ru' . "\r\n");
    exit;
}


// ---------------------------------------------------------------------
if ($cntr->errMsg != '')
    mail(MAIL_FROM, 'Moxa error', $cntr->errMsg, 'From: moxa@eldin.ru' . "\r\n");

while($executed) {
    pcntl_wait($status);
    $executed--;
}

unlink($lock_file);
?>


Единственный недостаток это то что нельзя использовать для коммерческого учета электроэнергии так как система не сертифицирована, ибо opensource и сертификация понятия взаимоисключающие. Кстати ни у кого нет протокола обмена для концентраторов меркурий 225.11 и 225.12?
как использовать php на клиентской стороне (формирование web-страниц пользователю) мне понятно, а как вы цикличность опрос+БД реализовали (при том что сбор данных это независимо выполняемая задача от просмотра этих данных)?
На FreeBSD в cron'е выполняем приведённый выше php'ник каждые 5 минут. Чтобы не было дважды запущенных периодических задач, создаём лок-файл.
как php встраивается в html и/или генерит html файл я понимаю… а как cron в данном случае запускает скрипт? и почему php тогда выбрали в этом случае?
Вот такие строки в /etc/crontab:
#minute hour mday month wday who command
*/5 * * * * <имя пользователя> php /usr/local/<путь>/electric_accounting_counters.php db_dir=<имя БД>

Как когда-то давно говорил мой научный руководитель, можно знать один язык программирования, два, три, а потом вы уже знаете все. Утрированно конечно, но близко к истине. Если серьёзно, то не видим причин плодить сущности, когда php устраивает.
статью плюсануть не успел (срок голосования истек), плюсанул в карму…
статьи не пишутся в том числе потому. что это занимает немало времени (а тем более хорошие, полноЦЕННЫЕ, с картинками)… сами видите — написали через три года…
у меня есть два ынтерпрайз проекта, по которым можно статьи сделать, но времени нет…
так глядишь и в cpp кто-нибудь переведет ваш код…
насчет документации на сайте openSCADA — регистрируйтесь и дополняйте wiki своими примерами использования той или иной сущности (примеров использования там очень не хватает)… это же open source…
хотя уже не столько примеры нужно сколько уже время патчей (некоторые вещи в QT гвоздями прибиты, например, поле ввода перечня атрибутов у параметра в узле Modbus — строка в 101 символ, а потомо перенос на строки и неважно что монитор 24" 1920х1200 и еще полно свободного места) и элементов (свой протокол для ширпотребной шалабушки, свои виджеты )… сколько лет уже разрабатывается продукт…
Патчи Роман прекрасно принимает. Так что правьте и предлагайте.
я про те, которые расходятся с идеологией и дизайнерским вкусом Романа… а так да, принимает… если…
Ну, в таком случае или терпеть, или искать другие пути. Например мне не нравится ни стиль, ни архитектура приложения. Но писать свое пока нет времени.
Ну и да, при создании модулей для рабочей ветки без внесения в общую кодовую базу нужно постоянно следить за каждым чихом. Внешне API может даже и не поменяться, но ваш модуль уже не будет работать должным образом.
Возможно, будет кому-то будет интересна программка для сбора статистики и получения инфо под OpenWrt роутеры. Поставил больше десятка мини роутеров с этой програмкой и подключением по RS485 через USB переходник (~1$ на Алиэкспресс), статистика собирается удалённо для внутреннего учёта — полёт нормальный уже год.
Под всякие малины и серверы умного дома типа homeassistant или majordomo не планировали портировать?
Системы не использую, планов пока нет.
Ну отправить по mqtt наверное не сложно. А на малине с дебианом может завеститись этот код?
Конечно. Я на Debian отлаживаю.
Осмелюсь предложить свою самоделку для Меркурий 206 под OpenWrt (JSON для Zabbix умеет) — GitHub. Подборка некоторых ссылок и недописанные мои заметки здесь — ZFT Lab.
Фидбек, тесты и правки приветствуются. Спасибо!
Only those users with full accounts are able to leave comments. Log in, please.