Hi,
I’ve just got the Octopus plug-in solar kit (FoxESS M1-800-E) but I couldn’t find a working integration that works with this invertor and doesn’t need a Pi based local gateway. I tried the FoxESS cloud API with a personal API key, and it does seem to work, although it is a bit fiddly to get the auth right.
You need an API key from FoxCloud > profile > User Profile > API Management.
The tricky bit is the signature. Every call needs an md5 of the path, your key and a timestamp, joined with a literal \r\n. HA can work that out in a template, but !secret doesn’t work inside a template, so I put the whole signature template in secrets.yaml. The timestamp and signature headers get rendered separately, so I round the timestamp to 10 seconds to make sure they match.
secrets.yaml:
foxess_api_key: "YOUR_API_KEY"
foxess_signature: '{{ ("/op/v1/device/real/query" ~ "\\r\\n" ~ "YOUR_API_KEY" ~ "\\r\\n" ~ ((as_timestamp(now()) // 10 * 10 * 1000) | int)) | md5 }}'
configuration.yaml:
rest:
- resource: https://www.foxesscloud.com/op/v1/device/real/query
method: POST
scan_interval: 300
headers:
Content-Type: application/json
lang: en
token: !secret foxess_api_key
timestamp: "{{ (as_timestamp(now()) // 10 * 10 * 1000) | int }}"
signature: !secret foxess_signature
payload: '{"sns": ["YOUR_SERIAL"]}'
sensor:
- name: "Solar Inverter Output Power"
device_class: power
state_class: measurement
unit_of_measurement: "W"
value_template: "{{ (value_json.result[0].datas | selectattr('variable', 'eq', 'generationPower') | map(attribute='value') | first | float(0) * 1000) | round(0) }}"
- name: "Solar Inverter Energy Total"
device_class: energy
state_class: total_increasing
unit_of_measurement: "kWh"
value_template: "{{ value_json.result[0].datas | selectattr('variable', 'eq', 'generation') | map(attribute='value') | first | float(0) }}"
Every other value is the same pattern with a different variable name. It polls every 5 mins.
There’s a full list of the variables Fox can send here: 4. Telemetry Variable Dictionary - FoxCloud OpenAPI. My M1 only sends about 44 of them though, as most are for batteries and three-phase systems.
A couple of examples of what you can do with them.
The running state comes back as a number, so I turn it into text. This goes in the same sensor list (163 is on-grid, 165 is fault):
- name: "Solar Inverter State"
value_template: >
{% set names = {
'160': 'Self-test',
'161': 'Waiting',
'162': 'Checking',
'163': 'On-grid',
'164': 'Off-grid',
'165': 'Fault',
'166': 'Permanent fault',
'167': 'Standby',
'168': 'Upgrading',
'169': 'Factory test',
'170': 'Illegal'
} %}
{% set code = value_json.result[0].datas | selectattr('variable', 'eq', 'runningState') | map(attribute='value') | first %}
{{ names.get(code | string, 'Unknown') }}
I’ve also got one panel disconnected at the moment, and you can tell from the voltage. Mine reads about 31V on the connected one and about 8V with no current on the empty input, so I use a binary sensor with a 15V cut-off. It’s only available when the sun’s up, as it tells you nothing at night. This goes under the same resource, next to sensor::
binary_sensor:
- name: "Solar Panel 2 Connected"
device_class: connectivity
availability: "{{ is_state('sun.sun', 'above_horizon') }}"
value_template: "{{ (value_json.result[0].datas | selectattr('variable', 'eq', 'pv2Volt') | map(attribute='value') | first | float(0)) > 15 }}"
I also have a Tapo P110 on the socket, and the lifetime energy matches within 0.5%. I put the Tapo stats into the built-in Energy dashboard for the solar side, as it updates every few seconds, whereas this only polls every 5 mins.
![]()