Як я створив власну систему моніторингу для домашньої сонячної станції з dSolar

Вітаю, спільното.

Я Едуард, фрілансер. Мене не стосується поняття deadline, я не обмежений рамками часу робочого дня. Тому отримую якісний продукт — бо програмую в умовах своєї максимальної продуктивності. Я беру думки, враження, ідеї та за допомогою програмування створюю з них щось матеріальне та корисне — те, що впливає на нас.

На прикладі системи моніторингу сонячної станції dSolar, яку я використовую для моніторингу обладнання власної автономної сонячної станції, спробую пояснити загальні принципи побудови системи та нюанси роботи з різнотипним обладнанням. А також описати методи обробки даних від сенсорів та прийоми, які полегшують цю обробку.

Чому я написав власну програму, а не користуюсь однією з існуючих — пояснював у статті Моніторинг домашньої сонячної станції: чому я написав власну систему.

dSolar — це не перша моя розробка, яка безпосередньо обробляє дані з різнотипного обладнання. До цього були:

  • GPSM — система GPS-моніторингу;
  • AGBControl — система контролю логістичних процесів на елеваторах.

Важливо

Приклади, наведені в статті, можна буде використати в будь-яких системах, що обробляють дані від сенсорів. Незалежно від того, яку мову програмування ви використовуєте.

Система побудована по стандартній дворівневій архітектурі клієнт-сервер. Мова програмування — Tcl. Дані зберігаються під управлінням SQLite. Інструментарій, бібліотеки, віджети — AndroWish. Система мультиплатформна. Сервер (dSolard) доступний на Linux і Raspberry. Клієнт (dSolar) на Windows, Linux, Android.

DSolarLite — це клієнт, в який інкапсульовано код сервера для зручності установки та експлуатації на пристроях, які виступатимуть середовищем зберігання даних й інтерфейсом користувача одночасно. DSolarLite доступний на Linux та Android.

Обладнання, з якого отримуються значення сенсорів, це:

  • два однофазні гібридніх інвертори Deye;
  • SmartShunt від Victron;
  • MPPT зарядний пристрій від Epever;
  • чотириканальне реле.

База даних

База даних містить інформацію від приблизно 80 сенсорів. Секундний інтервал зберігання даних перетворює це в 30 ГБ дискового простору за рік. За півтора роки експлуатації системи проблем зі швидкістю роботи з базою даних не виникало.

Але з погляду прагматизму вирішив обмежити строк зберігання даних з секундним інтервалом до 30 днів. Десятисекундні дані не обмежуються і доступні за весь період.

Для ефективного отримання узагальнених даних за широкі періоди (день, місяць, рік) на секундні (ті, в яких зберігаються дані з секундним інтервалом) таблиці встановлені тригери для зберігання даних в окремих таблицях з періодичністю 10 секунд, одна хвилина, одна година, один день.

Приклад тригера для десятисекундної таблиці, що зберігає дані сенсорів входу інвертора для сонячних панелей:

  create trigger pv_insert after insert on pv
  begin
          insert into pvtenseconds values(NEW.equipment_id, NEW.chan, NEW.clockseconds/10*10, NEW.u, NEW.i, NEW.p, NEW.temp, 1) 
      on conflict(equipment_id,chan,clocktenseconds)
      do update set u = u + NEW.u,
                             i = i + NEW.i,
                             p = p + NEW.p,
                             temp = temp + NEW.temp,
                             cnt = cnt + 1;
  end;

Де структура таблиці pv:

  create table pv
  (
    equipment_id    integer not null,
    chan            integer not null,
    clockseconds    integer not null,
    u               real not null,
    i               real not null,
    p               real not null,
    temp            real,
  
    constraint equipment_id_chan_clockseconds unique(equipment_id, chan, clockseconds),
    
    FOREIGN KEY(equipment_id) REFERENCES equipment(id) on update restrict on delete restrict
  );

Та структура таблиці pvtenseconds:

  create table pvtenseconds
  (
    equipment_id    integer not null,
    chan            integer not null,
    clocktenseconds integer not null,
    u               real not null,
    i               real not null,
    p               real not null,
    temp            real,
    cnt             integer,
  
    constraint equipment_id_chan_clocktenseconds unique(equipment_id, chan, clocktenseconds),
    
    FOREIGN KEY(equipment_id) REFERENCES equipment(id) on update restrict on delete restrict
  );

Важливим моментом при роботі з базою даних є мінімізація дискових операцій. Не можна записувати дані в базу даних одразу після отримання їх від сенсорів. Вас чекає кілька сотень операцій за секунду. SD-картка Raspberry при такому навантаженні вийде з ладу за місяць-два. Мінімізація дискових операцій в подібних системах — необхідність і обов’язок.

Я реалізував мінімізацію через створення бази даних в пам’яті зі структурою таблиць аналогічній секундним таблицям основної бази.

Це таблиці для зберігання значення сенсорів по:

  • pv — дані від сонячних зарядних пристроїв;
  • bat — дані по акумуляторній батареї;
  • grid — дані по електро-мережі загального користування;
  • load — дані по споживанню електроенергії домашнім обладнанням.

База даних в пам’яті приєднана до основної бази за допомогою стандартного механізму SQLite — attach database.

dbcmd eval «attach ’:memory:’ as dbmem»

За десятисекундним таймером відбувається копіювання в таблиці основної бази, тобто безпосередньо на диск, з наступним очищенням таблиць бази даних в пам’яті.

  dbcmd transaction immediate {
    dbcmd eval "insert or replace into pv select * from dbmem.pv order by clockseconds"
      dbcmd eval "delete from dbmem.pv"
    dbcmd eval "insert or replace into bat select * from dbmem.bat order by clockseconds"
      dbcmd eval "delete from dbmem.bat"
    dbcmd eval "insert or replace into grid select * from dbmem.grid order by clockseconds"
      dbcmd eval "delete from dbmem.grid"
    dbcmd eval "insert or replace into load select * from dbmem.load order by clockseconds"
      dbcmd eval "delete from dbmem.load"
  }

Монітор дискових операцій ядра iotop фіксував операції запису раз на десять секунд зі швидкістю ~380 КБ/сек.

Клієнт

Клієнт постійно взаємодіє з сервером. Інакше і бути не може, нам необхідно частіше ніж раз на секунду отримувати, обробляти, та візуалізувати дані від сенсорів.

Комунікація між сервером та клієнтом відбувається двома способами:

  • Перший — загальновідомий «Запит-Відповідь». Він використовується в моменти, коли необхідна обробка даних за запитом користувача.
  • Другий — «Відповідь без Запиту», коли клієнт приймає інформацію від сервера про поточний стан сенсорів без ініціювання отримання даних (без запиту). В цьому випадку ініціатором виступає сервер.

Процедури клієнта, які отримують дані від сервера, синтаксично описуються так, щоб мати змогу виконання як у спосіб «Запит-Відповідь», так і у спосіб «Відповідь без Запиту».

Звичайно, на клієнті можна було б організувати опитування по таймеру сервера за способом «Запит-Відповідь». Але це не ефективно — клієнт не знає, коли дані, які необхідно обробити для візуалізації, є на сервері й готові до відправки.

Процедура клієнта за вказівкою сервера запускається так:

 proc Protocol:SocketHandler:Receive { procanswer data } {
    {*}$procanswer $data
  }

Обидва методи працюють в асинхронному режимі.

Для мінімізації ефекту «закляклого вікна», коли інтерфейс не реагує на події від користувача, в клієнті широко використовується парадигма coroutine, яка реалізована стандартними методами Tcl.

Наприклад:

   # запускаємо візуалізацію даних по сенсорах сонячних панелей в режимі співпрограми
  coroutine PV:Coroutine[clock microseconds] PV:Coroutine $lpv
  
  proc PV:Coroutine { lpv } {
    # проходимось циклом по списку значень сенсорів
    foreach el $lbat {
      # обробляємо та візуалізуємо на екрані дані по конкретному сенсору
      PV:Indicator:Visual $el
      after idle [info coroutine]; 
      # призупиняємо виконання програми в цьому місці та передаємо управління циклу обробки подій
      # Цикл обробки подій перевіряє чи існують відкладені події та виконує їх
      yield
      # цикл обробки подій не містить відкладених задач
      # продовжуємо виконання програми з цього місця
    }
  }

Сервер

Сервер реалізовано на багатопотоковій технології. Основний потік виконує пред/пост стартові операції, приймає запити від клієнтів і відразу передає їх в потік обробки даних. Потік обробки даних організовано як thread pool.

Запускаємо пул потоків з трьома потоками:

  tpool::create -minworkers 3 -maxworkers 3 -initcmd {
    # тут розміщуємо код по ініціалізації потоків
    # завантажуємо бібліотеки
    foreach p { sqlite3 Thread } {
      package require $p
    }
    sqlite3 dbcmd $dbfile -readonly 1
    ...
    ...
    
    Log "ThreadSelect Start in [thread::id]" 1
  } -exitcmd {
    catch { dbcmd close }
    Log "ThreadSelect Exit from [thread::id]" 1
  }

Для кожного обладнання, що працює по протоколах «запит-відповідь» — наприклад, modbus, створюється окремий потік. Для обладнання, що працює по протоколах з постійною трансляцією даних сенсорів (Victron SmartShunt, чотириканальне реле) — створюється один загальний потік.

 thread::create {
    # тут розміщуємо код по ініціалізації потоку
    # завантажуємо бібліотеки
    foreach { s } { Thread sum } {
      package require $s
    }
    ...
    ...
    
    Log "ThreadEquipment Start in [thread::id]" 1
    
    thread::wait
    Log "ThreadEquipment Exit from [thread::id]" 1
  }

Запис та модифікація даних в базі даних SQLite відбувається виключно в одному потоці. Я усвідомлено обрав таку модель, хоча SQLite підтримує «одночасну» модифікацію даних з кількох потоків.

Для всіх інших потоків база даних відкривається виключно в режимі читання. Один потік пише, інші тільки читають. В Tcl дуже просто організована робота з передачі даних між потоками, включно з асинхронністю. Тому прийняти та обробити дані в потоці обладнання і передати їх в потік для запису — не проблема.

Передача даних в пул потоків:

  tpool::post -detached -nowait $ідентифікатор_пула_потоків $дані_для_передачі

Передача даних в потік:

  thread::send -async $ідентифікатор_потоку $дані_для_передачі

Де дані_для_передачі — це найменування процедури (та її параметри), яка повинна виконатись в іншому потоці.

Обладнання

Від інвертора Deye дані по modbus можна отримати двома шляхами: через спеціальний порт rs485 або через стандартний wifi-логер. Кількість регістрів, дані з яких необхідно отримувати, доходить до 80. Регістри, «дякувати» розробникам Deye, розміщені не послідовно. Один і той самий параметр, наприклад, вхідний струм МППТ-трекера, розміщений в різних регістрах для різних типів інверторів (трифазні, однофазні, мережеві).

Отримання даних від не послідовно розміщених регістрів необхідних даних вирішується двома способами:

  • кількома послідовними запитами значень регістрів які все ще розміщені послідовно.
  • одним запитом всієї послідовності, включно зі значенням зайвих регістрів, бо вони вклинились в послідовність необхідних регістрів.

Перший спосіб працює набагато повільніше. Я використовую зчитування значень одним запитом. В будь-якому випадку — це момент, який доведеться з’ясувати індивідуально для вашого конкретного обладнання.

Через rs485 час отримання значень 116 регістрів одним запитом становить ~280 мілісекунд. Час отримання значень через wifi ~400 мікросекунд.

Таймер, який щосекунди буде відправляти запит на отримання даних по modbus, повинен асинхронно запускати процедуру отримання даних. Інакше відбуватиметься зсув таймера на час отримання запиту.

Victron SmartShunt самостійно щосекунди відправляє дані сенсорів. Все, що необхідно — запустити процедуру обробки даних по події появи даних на комунікаційному порту.

Автоматична та періодична передача значень усіх сенсорів обладнанням — це приклад ідеального сенсора для програміста.

Якщо при виборі обладнання ви натрапите на варіанти, що працюють за таким принципом, обирайте їх. Це допоможе стимулювати розробників створювати правильне обладнання.

Додаткові матеріали

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Вітаю, Едуарде!
Дякую за вашу роботу, дуже крута система.
Користуюся dSolarLite, але планую встановити окремий Linux сервер, і реле.
Питання по реле.
Я дивився, яке реле ви пропонуєте, у продавця на Алі є декілька варіантів: 2,4,8 канальні реле.
Скажіть, будь ласка, буде працювати тільки 4-канальне, чи будь яке з вказаних?
також цікавить чи можна поставити їх декілька штук?
Бо, якщо мова про 4-канальне, то це має бути підключення споживачів зіркою через щиток.
У випадку існуючої прихованої проводки в будинку краще спрацювало б мати декілька реле, і встановлювати їх безпосередньо біля приладів (бойлер, кондиціонер, тощо).
Дякую за відповідь.

8-канальне точно не буде працювати без модифікації програми (мається на увазі працюватимуть тільки чотири канали). Двоканальне — повинно працювати. В будь-якому разі, якщо купите 8-канальне — доробити програму не проблема.
Кількість реле(самостійних, окремих плат) в системі необмежена.

Ще забув. Краще, хай вбудоване реле керує зовнішнім контактором.
Я так зробив.

ну так, це зрозуміло, реле має відповідати навантаженню. тільки через контактор.

Вчора мав доступ до двоканального реле. Висновок:
Працює не тільки чотириканальне реле а і двоканальне. Думаю не буде проблем і з 8, 16 та 32 канальними

Підписатись на коментарі