Integracja falownika Fronius Symo z systemami Smart Home

Systemy fotowoltaiczne odgrywają kluczową rolę w nowoczesnym zarządzaniu energią. Falownik Fronius Symo oferuje doskonałe możliwości integracji ze środowiskiem Smart Home. Dzięki otwartemu, lokalnemu API pobieranie danych jest niezwykle proste i nie wymaga stałego dostępu do chmury producenta. Takie lokalne podejście gwarantuje wysoką niezawodność i szybki czas reakcji dla automatyzacji.
Autorski skrypt PHP i widżet na smartfonie
Do monitorowania statusu instalacji PV służy dedykowany skrypt PHP uruchomiony na lokalnym serwerze. Skrypt ten odpytuje endpoint API falownika pod adresem [http://192.168.XXX.XXX/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceId=1&DataCollection=CommonInverterData]. Otrzymana odpowiedź w formacie JSON jest dekodowana w celu wyodrębnienia kluczowych parametrów: aktualnie generowanej mocy (PAC) oraz dziennego uzysku energii (DAY_ENERGY).
Odświeżanie danych odbywa się cyklicznie, na przykład co 5 minut, co pozwala na bieżące śledzenie produkcji. W przypadku, gdy falownik przechodzi w stan uśpienia (np. w nocy, gdy produkcja spada do zera, a wartość DAY_ENERGY zwraca błąd lub wartość pustą), zastosowano mechanizm awaryjny. Skrypt odczytuje wówczas ostatnie znane wartości z lokalnego pliku 1.dat, zapobiegając błędom wyświetlania.
Od strony wizualnej skrypt posiada dynamiczne tło, które zmienia kolor w zależności od aktualnie generowanej mocy. Dzięki specjalnej funkcji matematycznej generującej gradient, tło płynnie przechodzi między kolorami (np. od białego, przez pomarańczowy, aż po czerwony) na podstawie wartości PAC. Tak przygotowana strona, wyposażona w mechanizm autoodświeżania, może zostać użyta w aplikacji „Widgetify”, tworząc wygodny i spersonalizowany widżet bezpośrednio na ekranie głównym smartfona.
<?php
// Adjust the IP Address
$x = json_decode(file_get_contents("http://192.168.XXX.XXX/"
."solar_api/v1/GetInverterRealtimeData.cgi?".
"Scope=Device&DeviceId=1&DataCollection=CommonInverterData"), false);
$fallback1 = "";
$fallback2 = "";
if(!isset($x->Body->Data->DAY_ENERGY->Value) ||
$x->Body->Data->DAY_ENERGY->Value === null ||
$x->Body->Data->DAY_ENERGY->Value === false) {
$y = explode("|", file_get_contents("./1.dat"));
$fallback1 = $y[0];
$fallback2 = $y[1];
}
else
{
file_put_contents("./1.dat",
$x->Body->Data->PAC->Value.$x->Body->Data->PAC->Unit.
"|".$x->Body->Data->DAY_ENERGY->Value.$x->Body->Data->DAY_ENERGY->Unit);
}
if(isset($_GET["x"])) {
exit($x->Body->Data->PAC->Value.$x->Body->Data->PAC->Unit.
$fallback1."|".$x->Body->Data->DAY_ENERGY->Value.
$x->Body->Data->DAY_ENERGY->Unit.$fallback2."|".
date("H:i:s").($fallback1 != "" ? " F" : ""));
}
function Gradient($HexFrom, $HexTo, $ColorSteps) {
// credit: Hailwood (stackoverflow)
$FromRGB['r'] = hexdec(substr($HexFrom, 0, 2));
$FromRGB['g'] = hexdec(substr($HexFrom, 2, 2));
$FromRGB['b'] = hexdec(substr($HexFrom, 4, 2));
$ToRGB['r'] = hexdec(substr($HexTo, 0, 2));
$ToRGB['g'] = hexdec(substr($HexTo, 2, 2));
$ToRGB['b'] = hexdec(substr($HexTo, 4, 2));
$StepRGB['r'] = ($FromRGB['r'] - $ToRGB['r']) / ($ColorSteps - 1);
$StepRGB['g'] = ($FromRGB['g'] - $ToRGB['g']) / ($ColorSteps - 1);
$StepRGB['b'] = ($FromRGB['b'] - $ToRGB['b']) / ($ColorSteps - 1);
$GradientColors = array();
for($i = 0; $i <= $ColorSteps; $i++) {
$RGB['r'] = floor($FromRGB['r'] - ($StepRGB['r'] * $i));
$RGB['g'] = floor($FromRGB['g'] - ($StepRGB['g'] * $i));
$RGB['b'] = floor($FromRGB['b'] - ($StepRGB['b'] * $i));
$HexRGB['r'] = sprintf('%02x', ($RGB['r']));
$HexRGB['g'] = sprintf('%02x', ($RGB['g']));
$HexRGB['b'] = sprintf('%02x', ($RGB['b']));
$GradientColors[] = implode(NULL, $HexRGB);
}
$GradientColors = array_filter($GradientColors, function($val){
return (strlen($val) == 6 ? true : false );
});
return $GradientColors;
}
function ColorFromGradient($n, $min, $max, $colors) {
$tablecolors = [];
$prevcolor = array_shift($colors);
foreach ($colors as $color) {
$tablecolors = array_merge($tablecolors, Gradient($prevcolor, $color, 10));
$prevcolor = $color;
}
$max = $max-$min;
$n-= $min;
if ($n > $max) $n = $max;
$ncolor = round(count($tablecolors)/$max * $n)-1;
return $tablecolors[$ncolor];
}
?><!DOCTYPE html><html>
<head><script>
function dR() {
setInterval(function(){
document.getElementById('f3').innerHTML += 'R...';
var xhr = new XMLHttpRequest();
xhr.open("GET", "./pvgirdcgi.php?x="+new Date(), true);
xhr.timeout = 5000;
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
document.getElementById('f1').innerHTML = xhr.responseText.split("|")[0];
document.getElementById('f2').innerHTML = xhr.responseText.split("|")[1];
document.getElementById('f3').innerHTML = xhr.responseText.split("|")[2];
}
}
xhr.onerror = function() {};
xhr.send();
}, 8000);
}
//setTimeout(function(){window.location.reload()},1000*20);
</script>
<style>
<?php
if(isset($gear) && $gear == true) {
?>
body{text-align:center;margin-top:50px;font-size:32px;}
<?php
}
?>
span {
text-shadow:
0 0 7px #fff,
0 0 10px #fff,
0 0 21px #fff,
0 0 42px #0fa,
0 0 82px #0fa,
0 0 92px #0fa,
0 0 102px #0fa,
0 0 151px #0fa;
font-family:Arial,Verdana;
}
</style>
</head><body <?php
if(isset($gear) && $gear == true)
{
?> onClick="document.querySelector('#pvx').style.display='block';"+
"document.querySelector('.loader').style.display='block';"+
"document.querySelector('#pvall').style.display='none';"+
"window.location.reload();"<?php
}
?>style="background:#<?php echo ColorFromGradient($x->Body->Data->PAC->Value,
0, 9000, ['FFFFFF', 'FFAA00', 'CC0000']); ?>">
<?php
if(isset($gear) && $gear == true) {
?>
<style>
.loader {
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style><div style="display:none;text-align:center;width:100%;
margin-left:-16px;margin-top:100px;" id="pvx">
<span style="text-align: center;width: 120px;display: inline-block;">
<span class="loader" style="display:none;"></span>
</span>
</div>
<?php
}
?>
<div style="<?php
if(@$gear != true)
{?>width:80px;<?php } ?>text-align:center;" id="pvall">
<span style="text-shadow: 1px 1px #ffffff;
font-family:Arial,Verdana;font-weight:bold;">PV</span><br /><br />
<span style="font-weight:bold;" id="f1"><?php echo $x->Body->Data->PAC->Value.
$x->Body->Data->PAC->Unit.$fallback1; ?></span><br />
<span style="" id="f2"><?php echo $x->Body->Data->DAY_ENERGY->Value.
$x->Body->Data->DAY_ENERGY->Unit.$fallback2; ?></span>
<br /><br />
<span style="" id="f3"><?php echo date("H:i:s").($fallback1 != "" ? " F" : ""); ?></span>
</div>
</body>
</html>
Możliwości w systemie Home Assistant
Ten sam endpoint API Froniusa otwiera ogromne możliwości dla rozbudowanych platform Smart Home, takich jak Home Assistant. Po skonfigurowaniu natywnej integracji Fronius, Home Assistant nieustannie odpytuje dokładnie te same dane.
Daje to następujące możliwości:
- Panel Energia (Energy Dashboard): Dane o wyprodukowanej energii zasilają wbudowany panel energetyczny, dostarczając długoterminowych statystyk, obliczeń opłacalności oraz poziomu niezależności od sieci energetycznej.
- Inteligentne automatyzacje: Dane o mocy w czasie rzeczywistym (
PAC) mogą służyć jako wyzwalacze (triggers). Na przykład pompa ciepła, klimatyzator lub ładowarka do aut EV mogą włączać się automatycznie, gdy produkcja ze słońca przekroczy zadany próg, co maksymalizuje autokonsumpcję.
Podsumowanie
Falownik Fronius Symo udostępnia czytelne dane w czasie rzeczywistym za pośrednictwem lokalnego API. Zarówno w przypadku zastosowania autorskiego skryptu PHP połączonego z aplikacją Widgetify do szybkiego podglądu na telefonie, jak i przy wykorzystaniu potężnego systemu Home Assistant do zaawansowanych automatyzacji, integracja tego falownika czyni zarządzanie energią słoneczną procesem wysoce efektywnym.
Komentarze: 4
Mechanizm awaryjny oparty na pliku
1.datjest sprytny i widać, że powstał z praktyki — nocne wygaszenie falownika wywraca każdy widżet pisany na kolanie.Mam jednak wątpliwość co do samej wartości: w nocy widżet pokazuje moc z ostatniego odczytu, czyli z popołudnia. Czy nie uczciwiej byłoby pokazać zero?
Zero też byłoby nieprawdą, tylko innego rodzaju: falownik nie zgłasza wtedy zera, on po prostu nie odpowiada. To są dwa różne stany i widżet, który je skleja, myli w obie strony.
Rozsądny podział przebiega między dwiema wartościami. Dzienny uzysk (
DAY_ENERGY) po zachodzie słońca faktycznie się nie zmienia, więc ostatni znany odczyt jest tu poprawną odpowiedzią. Moc chwilowa (PAC) zmienia się co chwilę, więc jej ostatni odczyt po kilku godzinach nie znaczy już nic.W skrypcie widać zresztą zalążek rozwiązania: doklejane
Fprzy wartości awaryjnej. Rozdzielenie tych dwóch pól — ostatni uzysk jako wartość, moc jako stan „brak odpowiedzi” z godziną ostatniego odczytu — kosztuje kilka linii i usuwa jedyny przypadek, w którym widżet wprowadza w błąd zamiast informować.Ten sam endpoint w skrypcie PHP i w integracji Home Assistant to argument, który przekonał mnie do lokalnego API zamiast chmury producenta.
Pytanie o częstotliwość: skrypt odpytuje falownik co 5 minut, integracja robi to znacznie częściej, a u nas działałyby równolegle. Czy Symo znosi taki podwójny ruch bez problemu?
Samo odpytywanie nie jest problemem — lokalne API Froniusa jest do tego przeznaczone i kilkusekundowy interwał zwykle mu nie szkodzi. Kłopot zaczyna się przy dwóch niezależnych systemach pytających jednocześnie, bo odpowiedzi potrafią się opóźniać, a falownik bywa też jedynym punktem dostępu do licznika Smart Meter.
Sprawdza się podział na jedno źródło i resztę czytającą z niego. Jeżeli w domu działa Home Assistant, to on jest naturalnym kandydatem: skrypt PHP pobiera wtedy gotową wartość z jego API zamiast dokładać własne zapytanie do falownika.
Przy okazji znika problem nocny, o którym mowa w artykule. Encje w Home Assistant mają własne oznaczenie niedostępności, więc stan „falownik śpi” nie wymaga już pliku awaryjnego — widżet czyta stan, a nie ostatnią zapamiętaną liczbę.