Tutorial: Odczyt rejestrów SunSpec z falownika PV przez Modbus TCP

Spis treści
Numer rejestru znaleziony we wpisie na forum działa. Działa do najbliższego firmware’u, do kolejnego modelu tego samego falownika, do chwili dołączenia drugiego urządzenia odkładającego tę samą wartość gdzie indziej. Wtedy numer przestaje się zgadzać i nic tego nie mówi: Modbus podaje to, co stoi pod tym adresem, a liczby prawdopodobnej nie da się odróżnić od poprawnej.
Z tego powodu istnieje SunSpec. To spis położony na przestrzeni rejestrów, a jego odczytanie kosztuje cztery kroki działające jednakowo w każdym falowniku z tą obsługą.

Dlaczego sam numer rejestru nie wystarcza
Modbus nie zna nazw, jednostek ani typów. Odczyt daje szesnaście bitów, a każda wykładnia tych bitów odbywa się po stronie czytającego. Rejestr o zawartości 1234 może oznaczać 1234 waty, 123,4 wolta albo 12,34 ampera – a protokół nic o tym nie mówi.
SunSpec dokłada trzy rzeczy, a wszystkie trzy stoją w samej przestrzeni rejestrów: znacznik mówiący, że spis istnieje, łańcuch bloków mówiący, jakie pomiary są i gdzie, oraz na każdą grupę wartości współczynnik skali mówiący, gdzie stoi przecinek.
Praktycznym skutkiem jest to, że raz napisany czytnik działa również przy następnym urządzeniu. Waży to więcej, niż brzmi, bo rozwiązanie alternatywne – tabela adresów na każdego producenta i każdy stan firmware’u – jest właśnie tym, co po cichu się dezaktualizuje.
Znalezienie adresu bazowego i znacznika
Spis zaczyna się pod jednym z trzech adresów, a dwa pierwsze rejestry mówią, czy zaczyna się właśnie tam. Zawierają cztery znaki SunS, jako wartość 32-bitowa 0x53756E53.
from pymodbus.client import ModbusTcpClient
c = ModbusTcpClient("192.168.1.50", port=502)
c.connect()
for basis in (40000, 0, 50000):
r = c.read_holding_registers(basis, count=2, slave=1)
if not r.isError() and r.registers == [0x5375, 0x6E53]:
print("SunSpec od", basis)
break
W tym krótkim kawałku kodu tkwią dwa kamienie potknięcia. Pierwszym jest przesunięcie o jeden, na którym każdy raz się przewraca: dokumentacja liczy rejestry od jedynki, a protokół adresuje je od zera. Adres wydrukowany w podręczniku jako 40001 jest na łączu przesunięciem 40000, a odczyt pod 40001 daje drugą połowę znacznika i żadnej zgodności.
Drugim jest adres urządzenia. Falownik z wbudowanym licznikiem odpowiada jako dwa urządzenia na tym samym połączeniu, każde z własnym identyfikatorem i własnym spisem SunSpec. Odczyt mocy licznika pod identyfikatorem falownika daje wartość, która nie jest błędem i nie jest prawdą. Które identyfikatory są w użyciu, pokazuje strona Modbus w interfejsie webowym urządzenia – i tam też Modbus TCP zostaje w ogóle włączony, bo w większości falowników jest fabrycznie wyłączony.
Przejście łańcucha modeli
Po znaczniku następuje szereg bloków, a każdy blok zaczyna się od dwóch rejestrów: numeru modelu i długości. Długość dodana do bieżącego położenia daje początek następnego bloku, a numer modelu 65535 kończy łańcuch.
pos = basis + 2
while True:
kopf = c.read_holding_registers(pos, count=2, slave=1).registers
modell, laenge = kopf[0], kopf[1]
if modell == 0xFFFF:
break
print(f"model {modell:5d} dlugosc {laenge:3d} dane od {pos + 2}")
pos += 2 + laenge
Zwykły falownik odpowiada trzema albo czterema modelami, a numery mówią, czym każdy z nich jest.
| Model | Zawartość |
|---|---|
| 1 | Wspólne: producent, oznaczenie modelu, numer seryjny, wersja firmware’u |
| 101, 102, 103 | Falownik jedno-, dwu-, trójfazowy – liczby całkowite ze współczynnikami skali |
| 111, 112, 113 | Te same pomiary jako liczby zmiennoprzecinkowe, bez współczynników skali |
| 160 | Poszczególne łańcuchy, jeden powtarzalny blok na każdy regulator MPP |
| 201 do 204 | Licznik, po jednym modelu na rodzaj przyłączenia |
To, czy urządzenie oferuje 103 czy 113, bywa zwykle ustawieniem, a nie właściwością. Wiele falowników ma przełącznik między liczbami całkowitymi ze współczynnikami skali a zmiennym przecinkiem, i to on wyznacza, która z dwóch rodzin modeli pojawi się w łańcuchu. Czytnik oczekujący jednego, a zastający drugie, nie melduje w ogóle żadnych pomiarów – objaw mylący dla ustawienia, którego zmiany nikt nie pamięta.
Współczynniki skali, typy danych i wartości zastępcze
Wewnątrz bloku modelu położenie wartości stoi w specyfikacji i liczone jest od początku danych. Prąd fazy A siedzi na przesunięciu 2, a należący do niego współczynnik skali na przesunięciu 5.
Model 103, dane od 40071
przesuniecie 1 A uint16 prad calkowity
przesuniecie 2 AphA uint16 faza A
przesuniecie 3 AphB uint16 faza B
przesuniecie 4 AphC uint16 faza C
przesuniecie 5 A_SF int16 wspolczynnik skali dla wszystkich czterech
40072 = 1234 wartosc surowa
40075 = 0xFFFE uzupelnienie do dwoch, czyli −2
1234 × 10⁻² = 12,34 A
Współczynnik skali jest wykładnikiem dziesiętnym ze znakiem i obowiązuje dla całej grupy wartości, a nie dla jednej. Dlatego pojawia się raz na cztery prądy i dlatego prąd odczytany bez niego daje liczbę stukrotnie za dużą – a ta w instalacji domowej nadal wygląda na wiarygodny pomiar.
W praktyce liczą się jeszcze trzy właściwości kodowania. Wartości zajmujące dwa rejestry stoją słowem starszym z przodu, co odpowiada konwencji Modbusa i co niemal każda biblioteka robi poprawnie sama z siebie. Pola tekstowe mają stałą długość i są dopełniane bajtami zerowymi, a nie nimi kończone. A wartość, której urządzenie nie prowadzi, nie wynosi zera, lecz jest wartością zastępczą: 0x8000 dla int16, 0xFFFF dla uint16, 0x80000000 dla int32. Czytnik traktujący je jako liczby melduje 65535 woltów – a reguła je odrzucająca ma trzy linie.
LEER = {"int16": 0x8000, "uint16": 0xFFFF,
"int32": 0x80000000, "uint32": 0xFFFFFFFF}
def wert(roh, typ, sf):
if roh == LEER.get(typ):
return None
return roh * (10 ** sf)
Odczyt jednej wartości od początku do końca
Wszystkie części razem dają czytnik, który model odnajduje, zamiast zakładać jego adres.
def modell_finden(c, basis, gesucht, slave=1):
pos = basis + 2
while True:
m, laenge = c.read_holding_registers(pos, count=2, slave=slave).registers
if m == 0xFFFF:
return None
if m == gesucht:
return pos + 2, laenge
pos += 2 + laenge
def vorzeichen(r):
return r - 0x10000 if r > 0x7FFF else r
start, _ = modell_finden(c, 40000, 103)
block = c.read_holding_registers(start, count=6, slave=1).registers
strom = block[1] # AphA, przesuniecie 2 liczone od jedynki
sf = vorzeichen(block[4]) # A_SF, przesuniecie 5
print(f"{strom * 10 ** sf:.2f} A")
Warto odczytywać cały blok jednym żądaniem, a nie każdą wartość osobno. Odczyt Modbusa kosztuje niezależnie od długości kilka milisekund czasu obiegu – sześć rejestrów w jednym wywołaniu kosztuje więc tyle, co jeden rejestr w jednym wywołaniu. Ważniejsze zaś: wszystkie sześć wartości pochodzi wtedy z tej samej chwili. Sześć osobnych odczytów prądu, napięcia i mocy daje trzy wielkości do siebie niepasujące, a zbudowane na nich sprawdzenie wiarygodności odzywa się bez powodu.
Uwaga o częstotliwości odpytywania: falownik nie jest bazą danych. Żądanie co pięć sekund jest bezproblemowe, raz na sekundę bywa w części urządzeń granicą, a szybciej wytwarza przekroczenia czasu wyglądające na usterki sieci. Same wartości pomiarowe i tak rzadko odświeżają się częściej niż co sekundę.
Przeniesienie całości do Home Assistant
Gdy adresy są znane, ten sam odczyt działa również opisowo. Integracja Modbus w Home Assistant zajmuje się typem danych i znakiem; wpisać trzeba jedynie współczynnik skali jako liczbę, bo integracja nie odczytuje go z urządzenia.
modbus:
- name: wechselrichter
type: tcp
host: 192.168.1.50
port: 502
sensors:
- name: "Prad faza A"
slave: 1
address: 40072
data_type: uint16
scale: 0.01
precision: 2
unit_of_measurement: "A"
device_class: current
state_class: measurement
scan_interval: 10
- name: "Moc czynna"
slave: 1
address: 40084
data_type: int16
scale: 1
precision: 0
unit_of_measurement: "W"
device_class: power
state_class: measurement
scan_interval: 10
Wpisany na stałe scale jest kompromisem tej drogi i warto odnotować, skąd się bierze. Współczynnik skali odczytano raz powyższym skryptem; stoi w urządzeniu i praktycznie nigdy się nie zmienia, ale gwarancji na to nie ma. Po aktualizacji firmware’u porównanie wartości wyświetlanej z ekranem falownika trwa dziesięć sekund i wyłapuje stukrotność natychmiast.
Ostatnie pole rozstrzyga, czy wartość trafi do statystyki energii, a nie tylko na kartę. Wartość mocy w watach to device_class: power ze state_class: measurement; stan licznika w kilowatogodzinach to device_class: energy ze state_class: total_increasing. Jedno z drugim nie jest wymienne, a wybrać jako źródło energii da się wyłącznie to drugie.