Читання параметрів з EP3000 Plus
УпирНещодавно я придбав собі інвертор фірми Must моделі EP3000 Plus. В мене одразу було бажання прикрутити його до Home Assistant і дивитись параметри не бігаючи до нього, а у вигляді красивих графіків. Отже отримавши його я одразу почав приготування, щоб насолоджуватися графіками параметрів. З цього і починається захоплива подорож у світ декомпіляції і програмування на python. Поїхали :)
Для тих кому ліньки все читати - все запрацювало, внизу лінк на гітхаб з моєю творчістю.
У виборі мені дуже допоміг пост https://diy.manko.pro/2022/11/28/uninterruptible-power-supply-solutions/, а особливо мене зацікавило в цьому інверторі саме наявність програми автора, тобто я розраховував отримати інвертор, все підключити і не напрягатись.
Отже є інвертор, в ньому є usb порт. Але встановлений він в одному кінці будинку, а моє робоче місце в іншому. Та і якось несолідно тримати дівайс постійно підключеним до робочого ноутбуку, тому ідея була покласти поряд raspberry pi, щоб вона читала дані і слала на сервер.
Проблема тільки в тому що в мене була стара raspberry pi 1, ще й без wifi. Usb-wifi адаптери мені потрібні були давно, тому я одразу замовив їх аж 3 :) Поки вони їхали я поліз встановлювати Home Assistant, запустив його у докер контейнері і думає що тут все ок. Наївний :)
Отримавши адаптери я встановив на raspberry debian, скачав весь софт, підключив до інвертора і приготувався до фіналу. Якби це було так цього тексту не було б. На цьому етапі я зрозумів що треба якось raspberry додати в Home Assistant. Як виявилось скрипт, який читає дані з інвертора використовує mqtt протокол щоб слати дані на брокер. А в дефолтній конфігурації Home Assistant такого брокера немає. Ок, поставим відповідний аддон... Стоп. Виявляється в докер версії неможна встановлювати аддони, ця можливість є тільки в версії Supervisor і у версії OS.
Supervisor варіант мені абсолютно не підходить, оскільки вона працює тільки в debian, навіть ubuntu, яка в мене на робочому ноуті і де саме я планував розмістити сервер Home Assistant, не працює.
OS версія також не підходить, оскільки вона є тільки для raspberry 2 і ODROID яких у мене немає. Та і встановлювати ще й Home Assistant на ту raspberry що була я не хотів. Вона дуже стара і там досить відчутно тормозить навіть консоль, навантажувати ще й цією хуйнею я не хотів.
Єдиний варіант який запрацював - це скачати образ і запустити його у virtualbox. З цікавого в мануалі сказано що воно працює тільки через ethernet, а через wifi ні, але я спробував і все працює. Встановим туди аддон Mosquitto broker і йдемо далі.
Отже існуючий клієнт справно підключився до Home Assistant, заанонсував існуючі сенсори (напруги, температури, etc), але даних не було.
Почитавши код клієнта, я написам простецький скріпт, який просто шле 1 команду 'F\n' в послідовний порт. І у відповідь отримав тишу. Додаткова проблема була в тому, що опису протокола взаємодії саме з інвертором не було і що саме за команду я шлю я не знав :) Пару годин гугла ясності не додало. Це перша команда яку шле скрипт і чому вона не працює було геть незрозуміло. Читання мануала інвертора щодо підключення usb теж було малоінформативне - ніяких перемикачів типу "увімкнути usb моніторінг", ніяких опцій налаштування.
Окей подумав я, а може воно десь заховано у їх пропрієтарній програмі? Отже качаєм їх софт на windows ноутбук, встановлюєм, підключаєм... і тиша, софт неможе знайти інвертор :(

Це був провал. Я одразу подумав на бракований дівайс і перспектива переписки з магазином і пересилання його туди-сюди навіювала тоску і печаль.
Але один момент мене бентежив. Що debian, що windows рапортували що на тому кінці usb хтось та є, а саме "QinHeng Electronics CH340 serial converter" тобто якась мікросхема послідовного порту, яка працює і уважно (або не дуже) мене слухає. Звісно проблема могла бути і між цією мікросхемою і мізками інвертора, але то ввижалось мені малоймовірним. І до того ж - якщо це так, то пересилати інвертор зараз я нікуди не буду.
У розпачі я звернувся до автора кліента, який люб'язно погодився мені допомогти. Я скинув йому посилання на фірмовий софт і він декомпілювавши його висунув теорію що оскільки мій дівайс новіший з точки зору заліза, то він може використовувати не звичайну передачу даних через послідовний порт, а використовуючи протокол Modbus.
Це вже було щось. Я не дуже розбираюсь у роботі з послідовним портом, а про Modbus почув уперше, але труднощів не боїмося :) Тим більше що одразу знайшовся python модуль для роботи з цим протоколом.
Я декомпілював фірмовий софт і почав по ньому лазити. Досить швидко я побачив що дійсно, використовується Modbus, щось в нього пишеться, щось читається, але списку команд я там не знайшов. Додатково мене збивало те, що у списку дівайсів на мій був схожий тільки EP3000M, що, як ви розумієте, наводило на погані думки що мій дівайс програмою не підтримується, незважаючи на те, що на сайті виробника ясно написано що підходить.
Лазивши по різним форумам я скачав ще пару сторонніх програм для роботи з інверторами, але жодна не побачила його.
Але десь у глибинах сайту виробника були каменти і серед них я побачив одну дивину - чувак радив вимкнути bluetoth і тоді програма запрацює. Незважаючи на дикість поради я бачив в ній логіку - bt модуль створює додаткові віртуальні COM порти, які фірмовий софт також сканує. І о диво - програма побачила інвертор і його цифри. Саме тут я почав матюкатись на погромістів які це писали - ну йобана, як так можна?!
Це був успіх! Тепер я впевнений що дівайс працює і що мені треба копати Modbus.
Нажаль метод тику не підійшов і я поліз шукати може хто вже с таким розбирався. Знайшов один проект, для абсолютно іншого інвертора, але я хоч бачив як з Modbus працювати. Досить швидко я виявив що основна команда що мені треба read_holding_registers.
До речі, а ви знали що на github можна тицьнути на функцію і з випадаючої підказки перейти на її код? Я ось не знав.

Отже команда read_holding_registers вимагає адресу, якийсь id і розмір скільки байт прочитати. Щось про адресу я бачив у декомпільованому коді! Ось:
[Modbus(30000, 1.0, true)]
[NotMapped]
public short? MachineTypeI { get; set; }
Окей, адреса схоже 30000, id напевно це унікальний айді клієнта, тобто мого скрипта, а розмір... ну давайте 8 байт прочитаєм, згідно MachineTypeI там повинно бути щось типу EP3000, саме так був підписаний інвертор у фірмовій програмі.
Пробуєм і... Нічого. Просто нема відповіді.
Думаєм далі, курим декомпільований код. Десь там знаходим id для кожного типу інвертора, а також цифру 26 припускаємо що це саме той id, і саме той розмір для читання.
Ще спроба - знову фейл.
Десь в інтернеті я побачив як можна виставити дебаг для Modbus і міг бачити конкретні байти що йдуть в сторону інвертора, з того боку поки тиша. Виглядало це десь так:
SEND: 0xa 0x3 0x75 0x30 0x0 0x1a 0xdf 0x79
Розкуривши специфікацію протоколу я вже бачив що 0xa це дівайс айді, 0x3 це команда Read Holding Registers, а 0x75 0x30 це адреса 30000. Це мені стало в нагоді.
Десь тут я зі здивуванням побачив що мій скрипт не конектиться до інвертора (без помилок чи ексепшенів). Дивно.
Я розумів що заходжу в глухий кут. Я мало знаю про послідовний порт, там є якісь параметри типу baud, stopbits, parity, але що конкретно вони значать я не знав. А навіть якби і знав - невідомо які вони повинні бути. Ну окрім baud - в коді фірмової програми я побачив що він 9600. До того ж в Modbus є ще штуки типу dst і взагалі це виглядало досить печально.
Окей, думаю я. В мене є працююча програма, то може є спосіб підслухати що там бігає між ноутом і інвертором? Тобто гуглим serial port sniffer. Перебравши з 5 програм я знайшов працюючу і нарешті зміг побачити які саме пакетики бігають по usb кабелю. І саме там я знайшов необхідні stopbits і parity, аналізатор сніфера прямим текстом про це і написав. Круто!

Спочатку ініціація з'єднання з цими параметрами, а далі я побачив щось знайоме:

0A 03 75 30 00 1A DF 79 ..u0..Яy
Ойвей, та це ж моя команда! Значить я шлю все правильно.
Виствляєм параметри, шлем команду і ура, є відповідь!
SEND: 0xa 0x3 0x75 0x30 0x0 0x1a 0xdf 0x79
RECV: 0xa 0x3 0x34 0x0 0x3 0x0 0xce 0x0 0x2 0x0 0x18 0xb 0xb8 0x8 0xfc 0x1 0xf3 0x9 0x1c 0x1 0xf3 0x0 0xd 0x1 0x4 0x1 0x3e 0x0 0xa 0x0 0x0 0x1 0xf 0x0 0x5 0x0 0x0 0x0 0x64 0x0 0x13 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x1 0x0 0x1 0xe3 0x60
Це все щось. Отже, думав я, програма шле цю команду щоб отримати MachineTypeI, тобто десь у цій відповіді повинно бути EP3000. Починам шукать. Перебравши з десять способ перетворити цей потік байт в текст я зрозумів що щось не те. Розкурювання таблиць utf8 і ascii не допомогло.
Тоді підем іншим шляхом. Треба прочитати якийсь інт, його в байтах побачити легше. Ось гарний піддослідний:
[Modbus(30005, 0.1, true)]
[Display(Name = "Grid voltage")]
Отже по адресі 30005 лежить Grid voltage. А стоп ні, це поганий піддослідний. Напруга показується з точністю до 0.1 вольта, а значить це float. А читати float з потоку байтів сильно важче. Тут ще я пам'ятав про такі штуки як big endian і little endian (те що вони існують і впливають на кодування float - це все що я про них знаю :)), які можуть додати свої сюрпризи, а який саме endian використовує інвертор я не знав.
О, ось хороший варіант:
[Modbus(30010, 1.0, true)]
[Display(Name = "Load Power")]
Підійде. Але скільки я не бився, я не отримав шось схоже на правильний результат.
Пішов дивитись код фірмової програми,як вони перетворюють потік байтів у цифри. І знайшов ще один приклад криворукості погромістів. Протокол Modbus досить заморочений і на кожне повідомлення там ще окремо є контрольна сума. Я використовував готову бібліотеку і тому мені не треба було цим заморочуватись. А чуваки заморочились і рахували і порівнювали контрольні суми вручну. *facepalm*
Тобто код програми мені не допоміг і я пішов знову дивитись пакетики, щоб побачити читання адреси 30010 і подивитись що там шлеться і приходить.
Ліричний відступ. Тикався я на raspberry, сидячи за робочим ноутом і підключившись до raspberry через ssh. А щоб посніфити пакетики мені треба було брати віндовий ноут, іти до інвертора, відключати raspberry, підключати віндовий ноут, запускати сніфер і програму, дивитись що вона показує, відключати ноут, підключати raspberry, йти назад. Додатково розваги додавало те, що сніфер після відключення usb тупо стирав весь свій вивід, тому я не міг насніфити пакетиків і потім комфортно в них копирсатись. Тому доводилось копіювати текст пакету і зберігати його у telegram Збережених повідомленнях :)
Отже сиджу на кортах в коридорі, дивлюсь як бігають пакетики і не розумію. Я бачу як в циклі читається адреса 30000, а 30010 чи 30005 ніразу не читалось, як так?
Повернувшись за стіл я знову прочитав 30000 адресу і почав уважніше дивитись на вивід і побачив дещо цікаве:
DEBUG:pymodbus.payload:[3, 206, 2, 24, 3000, 2330, 500, 2332, 500, 16, 373, 389, 12, 0, 271, 5, 0, 100, 20, 0, 0, 0, 0, 2, 1, 1]
Чекайте, ото 3000 схоже на модель. А поряд 2330 схоже на напругу помножену на 10 для зручності. А 500 це явно частота.
Отже дійсно, прочитавши 26 регістрів(?) починаючи з адреси 30000 я отримую всі параметри. Це мене неабияк порадувало, оскільки процес читання не сказати щоб відбувався досить швидко, read_holding_registers виконувалась десь 0.5 секунди і перспектива витрачати по півсекунди на кожен параметр не додавала оптимізму. А так - все за 1 раз.
Отже команда read_holding_registers повертає масив (list парселтангом :)) в якому послідовно, у вигляді int перечислені показники.
Далі було вже все просто - перетворити масив у конкретні змінні, з перетворенням у float де це потрібно це механічна, а не творча задача. Сформувати з них повідомлення для mqtt брокера - теж, я це піддивився в оригінальному скрипті.
Щоб знайти де саме який параметр лежить я піддивився у фірмову програму:
[Modbus(30009, 0.1, true)]
[Display(Name = "Load current")]
[DisplayFormat(DataFormatString = "{0} A")]
[Column(Order = 11)]
Я думав Column(Order = 11) це для якогось внутрішнього відображення, а воно вказує на індекс масива. До речі деякі параметри виявились не на своїх місцях, але то було дріб'язок - я просто подивився на панелі інвертора що саме означає ця цифра і відповідно поправив код.
Магія big endian/little endian мені не знадобилась, але для феншую я пошукав endian в коді фірмової програми і виявилось що little. Ну і хуй з ним.
Таким чином я написав готовий скріпт для читання параметрів інвертора і посилкою їх в Home Assistant, чим і вихваляюсь.
Наостанок пару посилань:
Оригінальний скрипт https://github.com/mkbodanu4/ep30_pro_mqtt
Я зробив пул-реквест автору, але поки він не змерджений, тому мій форк https://github.com/darkmind/ep30_pro_mqtt
*Апдейт після 3х місяців користування під картинками.
Ну і картиночок.


Апдейт через 3 місяці:
Мені не подобались графіки в home assistant, вони були незручні, а я звик до офігенної grafana. Спроби прикрутити графану до home assistant невдалися і в цей момент я зрозумів наскільки незручна система home assistant. Якогось додаткового функціоналу, окрім графіків, мені не треба було, тому я вирішив переїхати на Debian.
Додатковим моментом було те, що віртуалка з home assistant була досить жадна - 32Gb винт, 2Gb пам'яті, 2 процесори. У мене на ноуті 8Gb пам'яті, а тому це було відчутно. Debian віртуалки на 8Gb винт, 512Mb пам'яті і 1го процесора виявилось абсолютно достатьно.
Отже Debian, grafana, influxdb як база данних. До речі як виявилось потім в home assistant використовується sqlite для цього, що не є гарним рішенням для данних такого типу. Скрипт, який читає дані з інвертора шле їх напряму в influxdb, тобто я ще повикидав купу проміжних сервісів. Однозначно плюс.
Отже є influxdb+grafana а це трохи росширює поле діяльності. Наприклад в самій grafana є можливість прикрутити алерти по умовах і тепер коли зникає світло я отримую повідомлення в спеціальний чатик в Телеграмі - зручно.
Помацявши telegraf я зрозумів що це досить класний інструмент моніторінгу, який ще може читати (через свої плагіни) ще дофіга чого і слати в influxdb. То я одразу прикрутив його до моніторінга самої Raspberry (cpu, пам'ять, мережа, io). Заодно додав цей моніторінг до свого ноута ну і одним махом до роутера мікротік + алерти по температурі в Телеграм.
Отже є купа даних і я звернув увагу ось на який момент - cpu usage на Raspberry показував сплески io wait'ів, що досить погано. Погіршувало ситуацію те, що система Raspberry розташована на MicroSd картці, що не добре для її життя. 1ша ідея була трохи переробити систему щоб вона була read-only, а записи йшли в пам'ять і не виснажували картку. Але це досить складно і втрачати логи системи теж не хотілось. Отже спочатку треба ідентифікувати хто взагалі пише на "диск". Як виявилось (тут без несподіваток) це по суті тільки journald. Я підозрював, а гугл підтвердив, що journald вміє не писати локальні логи, а одразу слати їх на remote journald сервер. Що і було зроблено і з записів на картку лишилось тільки позиція лога journald, що вже відправлено на сервер, а що ні :)
Всесь цей новий сетап працював чудово і без нарікань, але ж немає меж для покращень :) Основний момент - мені доводилось обслуговувати 3 системи: робочий роут, Raspberry і віртуалку з Debian. І я задумався, а нащо взагалі ця віртуалка (і її оверхед)?
І тому зараз на ноуті запущено 2 докер контейнера, з influxdb, і з grafana відповідно. Звісно можна ще запускати сервіси напряму на ноуті, але тут є деякі обмеження - я не хочу підключати сторонні репозиторії до робочої системи. Якщо у вас немає таких обмежень - feel free :)
Отже так воно зараз і працює і мені подобається. Куди покращувати далі - ну хіба шо на виділений міні-сервер, але той можна буде підключити напряму до інвертора і відправити Raspberry на чергову пенсію :)