Skip to content

Instantly share code, notes, and snippets.

@mdixon4
Last active April 5, 2024 11:57
Show Gist options
  • Save mdixon4/e799eb1ff21fa56e7c34cc356915c2f1 to your computer and use it in GitHub Desktop.
Save mdixon4/e799eb1ff21fa56e7c34cc356915c2f1 to your computer and use it in GitHub Desktop.
Using excess solar over 2 phases with a Fronius Wattpilot

Making use of two phase solar generation with a Fronius Wattpilot EVSE

I have a non-typical two-phase power supply with 4.8kW solar generation on each phase. Each phase has a Fronius inverter (with datamanager card) and smart meter on the grid import/export. It calculates the household load as the difference between the generation and export.

I have recently installed a Fronius Wattpilot EVSE to charge my electric car. The Wattpilot can operate on one or three phase power, in my case it is limited to single-phase operation since I do not have a three-phase power supply.

The Wattpilot can communicate with a Fronius inverter to determine the excess solar generation, and adjust the EVSE's power draw accordingly. The inverter reports the excess solar generation by observing the grid import/export on the same phase.

The Wattpilot can only communicate with one inverter, and because our inverters are separate systems on separate phases, it can only know about the excess solar on one phase. However, with net-metering across two phases, we should enable the Wattpilot to draw an equivalent amount to the total excess solar across both phases. Even though this will mean importing power on the Wattpilot's phase, this will be offset by the export of excess power on the other phase.

A worked example:

Figure 1 Figure 1: The EVSE has a data link to the inverter on the same phase, so it knows the excess solar generation, and can adjust its load to match. It draws all the excess solar on this phase, and so there is no net import/export on this phase. However, the other phase is exporting all its excess solar to the grid.

Figure 2 Figure 2: If the EVSE could somehow learn the overall excess solar generation, it could draw that amount. It would require importing from the grid, but this would be offset by the export on the other phase, thanks to net metering.

Potential solutions

1. Network the inverters together

Using the DBUS interface, it is possible to network the two inverters together, and they can report as one. In this way one inverter can report the total generation. However, it is not possible to also communicate the smart meter reading from the other phase, meaning the inverter still only reports the grid export from the same phase (so the EVSE cannot know to draw any extra power).

2. Use a three-phase smart meter

Instead of using two single-phase smart meter, I investigated whether a single three-phase smart meter would solve the problem. Indeed, it did appear that it would be able to report the total grid export across both phases, which when combined with the networked inverters would report the correct value to the Wattpilot.

However, this might not be a long-term solution. The network operator limits export to 5kW per phase, and although at the moment our systems have a 4.8kW capacity on each phase, if we were to upgrade them they would require meters that can measure the export on each phase to avoid exceeding the export limit. I had the impression that by combining the meters and the inverters into a single system, we would lose the ability to control the export on each phase.

See Fronius's How to set up Export Limiting using the Fronius Smart Meter guide. According to section 2.4:

Activate power reduction by choosing 'limit for entire system'
...
If the system comprises multiple inverters, all inverters which are connected in the SolarNet ring to the Datamanager will be equally power limited to achieve the set output limit.

In other words, we can specify that the system should not export more than 10kW across both phases, but not that each phase should export only 5kW. If we upgraded our systems such that each phase could generate 8kW of solar after normal household load, the inverters would both be throttled a little to ensure the export was 10kW and not 16kW.

However, if the EVSE applied a large load on one of the phases, this would be enough to draw the total export below 10kW without having to throttle the inverters. But in this case, the other phase would then be unthrottled and would still be exporting at 8kW, beyond the allowed maximum.

There are no-doubt more sophisticated methods and/or equipment that can help here, but based on the Fronius documentation it seems that this is not a commonly supported use-case.

3. Create a mock inverter

The Wattpilot communicates via the inverters via the http solar API. It would theoretically be possible to create a mock server that a) polls both inverters' API, b) calculates the total excess solar, and c) presents its own API for the Wattpilot to poll. It also has to be able to be discoverable by the Wattpilot, which was the main challenge.

4. Offload EVSE control to a third-party app, e.g. Charge HQ

It is also possible to use the Open Charge Point Protocol (OCPP) to control the Wattpilot's charging behaviour via third-party service, such as Charge HQ. However, doing so would lose some of the benefits of the Wattpilot, for example the push-button interface to change modes. Furthermore, by default Charge HQ only connects to one inverter, so I would still need an intermediate step to aggregate the solar data. As well as this, the data would need to go via the cloud, which means it would be less reliable in case of network outages.

Based on this I thought option 3 (a mock inverter) would be preferable if I could get it to work.

Mock Inverter Implementation

The mock inverter has to do the following:

  1. Be discoverable by the Wattpilot.
  2. Find the inverters on the network. This can be done by hard-coding the IP addresses of the inverters or by using the same method that the Wattpilot uses to discover them.
  3. Serve a solar API endpoint for the wattpilot to poll, which returns the calculated total of the figures from each actual inverter.

I implemented this via a node.js server running on a raspberry pi.

1. Discovery

To enter discovery mode, use the Wattpilot app to scan for new inverters. When in discovery mode, The Wattpilot sends a UDP packet to the broadcast address (192.168.0.255) on my LAN with a payload such as { Query: { DeviceFamily: 'all', ReplyOnPort: 50052 }, V: 1 }. By mocking the same packet, I was able to determine the typical response of the inverters, which was a UDP packet back to the originating IP address, using the port "ReplyOnPort", with a payload such as { DeviceFamily: 'DataManager', Hardware: '2.4D', ... }. Once known, I could listen for the initial query from the Wattpilot and reply with a payload of my own, which made my mock server appear as an inverter in the Wattpilot app.

2. Find the inverters on the network

Although the inverter IP addresses can be hard-coded, the method above can also be used to find them dynamically. An example JS implementation is provided below, which both listens for the query and replies with a mock response, and also sends the query to the network and listens for the response.

import dgram from 'node:dgram'

const MOCK_DATA = {
  DeviceFamily: 'DataManager',
  Hardware: '2.4D',
  Id: '240.999999',
  Label: 'Mock Inverter',
  Model: 'WILMA2-0',
  Software: '3.28.1-3',
}

function tryParse (str) {
  try {
    return JSON.parse(str)
  } catch (err) {
    return
  }
}

export class InverterLocator {
  constructor() {
    this.inverters = {}
    this.socket = dgram.createSocket('udp4')
    this.socket.on('message', (msg, rinfo) => this._handleMessage(msg, rinfo))
    this.socket.on('listening', () => this._handleListening())
    this.inverterFoundCallbacks = []
  }

  _handleListening() {
    const address = this.socket.address()
    // console.log(`UDP server listening ${address.address}:${address.port}`)
  }

  _handleMessage(msg, rinfo) {
    // console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`)
    const message = tryParse(msg.toString('utf-8'))
    const ip = rinfo.address

    if (!message) {
      console.log('Could not parse ', msg.toString('utf-8'))
      return
    }

    if (message.Response) {
      if (message.Response.Id && message.Response.Id !== MOCK_DATA.Id) {
        this.inverters[message.Response.Id] = message.Response
        this.inverterFoundCallbacks.forEach(callback => callback({
          ...message.Response,
          IP: ip,
        }))
      }
    }

    if (message.Query) {
      if (message.Query?.IsMe) {
        // Ignore messages from self
        return
      }
      if (message.Query?.DeviceFamily === 'all' && message.Query?.ReplyOnPort) {
        const replyOnPort = message.Query.ReplyOnPort
        const replyIp = rinfo.address

        this._sendMessage(
          {
            Response: MOCK_DATA,
            V: 1,
          },
          replyIp,
          replyOnPort
        )
      }
    }
  }

  _sendMessage(data, destinationIP, destinationPort) {
    // Define the packet payload as a JSON string
    const jsonData = JSON.stringify(data)
    // console.log('Sending UDP packet', jsonData, destinationIP, destinationPort)
    // Convert the JSON string to a Buffer
    const bufferData = Buffer.from(jsonData, 'utf-8') // Node
    // const bufferData = new TextEncoder().encode(jsonData) // Deno

    // Send the UDP packet
    this.socket.send(bufferData, destinationPort, destinationIP, (err, bytes) => {
      if (err) {
        console.log(err)
        throw err
      }
    })
  }

  _sendQuery() {
    this._sendMessage(
      {
        Query: {
          DeviceFamily: 'all',
          ReplyOnPort: 50052,
          IsMe: true, // We add this IsMe flag so we can ignore our own messages
        },
        V: 1,
      },
      '192.168.0.255',
      50052
    )
  }

  onInverterFound(callback) {
    if (this.inverters.length > 0) {
      this.inverters.forEach(callback)
    }
    this.inverterFoundCallbacks.push(callback)
  }

  start() {
    this.socket.bind(50052, '0.0.0.0', () => {
      this.socket.setBroadcast(true)
    })
    setInterval(() => this._sendQuery(), 1000)
  }
}

const inverterLocator = new InverterLocator()

inverterLocator.onInverterFound(inverter => console.log('Inverter found', inverter))

inverterLocator.start()

3. Serve a solar API endpoint

The solar API endpoint that is required is /solar_api/v1/GetPowerFlowRealtimeData.fcgi. This is also the endpoint that needs to be fetched on each inverter. The response is a JSON body such as:

{
  "Body": {
      "Data": {
          "Inverters": {
              "2": {
                  "DT": 76,
                  "E_Day": 23087,
                  "E_Total": 33773000,
                  "E_Year": 1420560.125,
                  "P": 2401
              }
          },
          "Site": {
              "E_Day": 23087,
              "E_Total": 33773000,
              "E_Year": 1420560.125,
              "Meter_Location": "grid",
              "Mode": "meter",
              "P_Akku": null,
              "P_Grid": -2311.73,
              "P_Load": -89.26999999999998,
              "P_PV": 2401,
              "rel_Autonomy": 100,
              "rel_SelfConsumption": 3.718034152436484
          },
          "Version": "12"
      }
  },
  "Head": {
      "RequestArguments": {},
      "Status": {
          "Code": 0,
          "Reason": "",
          "UserMessage": ""
      },
      "Timestamp": "2024-03-08T15:48:15+11:00"
  }
}

The total figures under "Site" can be compiled by:

  • Summing E_Day, E_Total, and E_Year across all inverters
  • Leaving Meter_Location as "grid", Mode as "meter" and P_Akku as null
  • Summing P_Grid, P_Load, and P_PV across all inverters
  • Averaging rel_Autonomy and rel_SelfConsumption across all inverters. (NB: I do not think this is technically correct, but also I don't think this matters. The Wattpilot only seems to use the P_Grid, P_Load, and P_PV values.)

It is a matter of setting up a web server to respond to such requests at the same IP address and on port 80, which is a standard task and out of scope for this note.

Outcome

This solution works well. The Wattpilot is able to locate the mock inverter on the network and poll it for aggregated data. This means that in the example case, the Wattpilot will see a total generation of 8kW and a total grid export of 6kW, so it will draw 6kW to charge the car. This means it is technically importink 3kW from the grid on phase 1, but this is offset by the 3kW export on phase 2, so the net import/export is zero.

@mdixon4
Copy link
Author

mdixon4 commented Apr 5, 2024

fig1 fig2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment