Integrating the Fronius Symo Inverter with Smart Home Systems

Photovoltaic systems play a crucial role in modern energy management. The Fronius Symo inverter offers excellent connectivity for Smart Home environments. Thanks to its open, local API, data extraction is straightforward and does not require constant cloud connectivity. This local approach ensures high reliability and fast response times for various automations.
Custom PHP Script and Smartphone Widget Integration
To monitor the status of the PV installation, a custom PHP script running on a local server can be utilized. This script queries the inverter’s API endpoint at [http://192.168.XXX.XXX/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceId=1&DataCollection=CommonInverterData]. The JSON response is decoded to extract key metrics, specifically the current power output (PAC) and the total energy generated for the day (DAY_ENERGY).
The data is fetched periodically, such as every 5 minutes, keeping the information up to date. To handle periods when the inverter is offline—like during the night when solar production drops to zero and the DAY_ENERGY value returns as null or false—a fallback mechanism is implemented. In such cases, the script reads the last known values from a local 1.dat file, ensuring the display remains intact without errors.
Visually, the script features a dynamic background color that changes based on the current power generation. By using a custom gradient function, the background transitions smoothly through different colors (e.g., from white to orange to red) depending on the PAC value. This lightweight, self-refreshing webpage can then be displayed directly on a smartphone home screen using an application like “Widgetify”, creating a highly convenient and personalized monitoring widget:
<?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>
Home Assistant Capabilities
The exact same Fronius API endpoint is incredibly valuable for more complex Smart Home platforms like Home Assistant. By configuring the native Fronius integration, Home Assistant continuously queries the inverter’s JSON data.
This enables several powerful features:
- Energy Dashboard: The generated energy data feeds directly into the Home Assistant Energy Dashboard, providing long-term statistics, cost calculations, and grid dependency metrics.
- Smart Automations: Real-time power data (
PAC) can be used as a trigger for automations. For instance, a heat pump, air conditioning unit, or electric vehicle charger can be automatically turned on when the solar production exceeds a specific threshold, maximizing self-consumption.
Summary
The Fronius Symo inverter provides easily accessible, real-time data through its local API. Whether utilizing a custom PHP script linked to the Widgetify app for quick smartphone monitoring or deploying a comprehensive Home Assistant setup for advanced automations, integrating this inverter into a Smart Home ecosystem makes tracking and optimizing solar energy usage highly efficient.
2 comments
The fallback file is a neat solution to the sleeping inverter — the whole widget falls apart at dusk without something like it.
One thing about that file worries me: with auto-refresh on a phone widget and a browser tab open, two requests can land at the same moment. Is there a risk of reading a half-written file?
There is, and it is the kind of race that shows up as a single implausible reading rather than as an error — a value that is half of yesterday’s number, once a week, with nothing in the logs.
Two small changes remove it. Writing with an exclusive lock keeps a reader from seeing a partial file, and writing to a temporary file followed by a rename makes the replacement atomic on the same filesystem — the reader sees either the old content or the new one, never something in between.
The third habit matters more than either: the reader should tolerate nonsense. The file is a fallback, so an empty or malformed read should degrade to "no current reading" rather than to a number. That also covers the first run, when the file does not exist yet — which is the other moment this code path gets exercised and the one where a stray value looks most convincing.