Solax X1 Hybrid G4 (local & cloud API)

Having struggled with the default Solax integration, I’ve set myself a challenge of using the available local and cloud APIs instead. The main motivation behind this was to get real-time data from the inverter, particularly the grid in/out readings from the CT clamp so that I can maximise self-use. In addition, being able to charge the battery from the grid in case of low solar forecast would be a nice to have.

The end result looks like this:


… plus a few automations which I will cover later.

My setup is as follows:

  • Solax X1-Hybrid-G4 with Solax Pocket Wifi (X1-Hybrid-5.0-D; type=15; version=3.003.02)
  • Two Solax Triple Power HV10230V2 3.1kWh batteries
  • 6.48kW of solar panels (JA)
  • Container-based Home Assistant with the following integrations:

All the configuration has been split into the following files:

  • configuration.yaml
  • sensor.yaml (where the REST connectors are)
  • secrets.yaml (for the API keys and payloads)
  • powercalc.yaml (for the dashboards)
  • automations.yaml

Cloud APIs

The integration with the Solax Cloud API was straightforward thanks to @ColinRobbins and his great article. While this should be sufficient for most people, I was really keen on the real-time monitoring aspect. What is more, the cloud API is only refreshed every 5min and occasionally doesn’t work at all, so not having a hard dependency on Solax Cloud for basic home monitoring and automation was also beneficial.

Local APIs

Most importantly, the local connection does not need any extra configuration and can be polled as often as requred (in my case every 5 seconds). As if by magic, I can access my inverter on a local IP address (192.168.1.x) and issue the same commands as being on the Solax Pocket Wifi and using the 5.8.8.8 address to connect! Here is the curl command:

curl -d "optType=ReadRealTimeData&pwd=REG_NO" -X POST http://192.168.1.x

where REG_NO is the Solax Registration Number (e.g. SV2AMZFWPT).

I’ve based the local connection configuration on the following resources:

Most of the configuration has been copied from the referenced resources, but in order to distinguish between the cloud and local connections, all the cloud sensors have a solax_cloud prefix, and all the local ones solax_local.

In addition to reading the standard inverter data, I’ve managed to pull the inverter settings and update one of the self-use configurations. Here are the decoded fields for both real-time data and settings (read set data): X1 Hybrid G4 - Google Sheets - any help to decode more will be appreciated of course!

Configuration files

Below are complete snippets you can adopt to your needs - for all my setup please see my github repo.

secrets.yaml

solax_cloud_api: https://www.solaxcloud.com/proxyApp/proxy/api/getRealtimeInfo.do?tokenId=TOKEN&sn=REG_NO

solax_local_ip: http://192.168.1.x/ # IP of solax module

solax_local_realtime_payload: "optType=ReadRealTimeData&pwd=REG_NO"

solax_local_settings_payload: "optType=ReadSetData&pwd=REG_NO"

solax_local_set_battery_min_soc_payload: 'optType=setReg&pwd=REG_NO&data={"num":1,"Data":[{"reg":29,"val":"{{ level }}"}]}'

solax_local_set_battery_grid_charge_level_payload: 'optType=setReg&pwd=REG_NO&data={"num":1,"Data":[{"reg":31,"val":"{{ level }}"}]}'

solax_local_set_battery_forced_charge_start: 'optType=setReg&pwd=REG_NO&data={"num":1,"Data":[{"reg":37,"val":"{{ value }}"}]}'

solax_local_set_period2_enable_payload: 'optType=setReg&pwd=REG_NO&data={"num":1,"Data":[{"reg":41,"val":"{{ enabled }}"}]}'

You can obtain the tokenId and the registration number (REG_NO) from Solax Cloud.

sensor.yaml


###################################
# Solax X1 Hybrid G4 Rest Sensors #
###################################

# Solax Cloud REST
- platform: rest
  scan_interval: 595    
  resource: !secret solax_cloud_api
  json_attributes_path: "$.result"
  json_attributes:
      - yieldtoday
      - yieldtotal
      - acpower
      - uploadTime
      - inverterStatus
      - feedinpower
      - feedinenergy
      - consumeenergy
      - soc
      - batPower
      - powerdc1
      - powerdc2
      - batStatus
  value_template: 'Active'  # dummy value, not used; avoids the "State max length is 255 characters" error
  name: "solax_rest_cloud"

### Solax Local REST sensor ###
- platform: rest
  scan_interval: 5
  resource: !secret solax_local_ip
  payload: !secret solax_local_realtime_payload
  method: POST
  name: "solax_rest_local"
  json_attributes:
        - sn
        - ver
        - type
        - Data
        - Information
  value_template: 'OK'  # dummy value, not used; avoids the "State max length is 255 characters" error

### Solax Local REST settings ###
- platform: rest
  scan_interval: 3600
  resource: !secret solax_local_ip
  payload: !secret solax_local_settings_payload
  method: POST
  name: "solax_rest_local_settings"
  # Unfortunately settings are not returned as a JSON document but an array of numbers, so having to pick just the relevant to avoid the max 255 chars limit
  # 0 - Self Use Min SOC %
  # 1 - Self Use Charge from grid (0 for disabled, 1 for enabled)
  # 2 - Self Use Charge from grid to % 
  value_template: "{{ '[' ~ value.split(',')[28] ~ ',' ~ value.split(',')[29] ~ ',' ~ value.split(',')[30] ~ ']' }}"

configuration.yaml


# Loads default set of integrations. Do not remove.
default_config:

# Load frontend themes from the themes folder
frontend:
  themes: !include_dir_merge_named themes

powercalc:

logger:
  default: info
  logs:
     homeassistant.components.rest: info
#    custom_components.powercalc: debug

automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml

template:
  - sensor:
    - name: "Solax Cloud Yield Today"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'yieldtoday') }}"
      unit_of_measurement: "kWh"
      unique_id: solax_2
      icon: mdi:flash
      device_class: energy
      state_class: total_increasing

    - name: "Solax Cloud Yield Total"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'yieldtotal') }}"
      unit_of_measurement: "kWh"
      unique_id: solax_3
      icon: mdi:flash
      device_class: energy
      state_class: total_increasing

    - name: "Solax Cloud Inverter"
      unit_of_measurement: "W"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'acpower') }}"
      unique_id: solax_cloud_inverter
      icon: mdi:flash
      device_class: energy
      state_class: measurement

    - name: "Solax Cloud Upload Time"
      state: >
        {% set time = state_attr('sensor.solax_rest_cloud', 'uploadTime') %}
        {{ as_timestamp(time) | timestamp_custom('%I:%M %p') }}
      unique_id: solax_5
      icon: mdi:clock

    - name: "Solax Cloud Battery Status"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'batStatus') }}"
      unique_id: solax_6
      icon: mdi:battery

    - name: "Solax Cloud Solar Panel"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'powerdc1') + state_attr('sensor.solax_rest_cloud', 'powerdc2') }}"
      unit_of_measurement: "W"
      device_class: energy
      state_class: measurement
      unique_id: solax_cloud_solar_panel
      icon: mdi:solar-power-variant

    - name: "Solax Cloud Battery Use"
      state: >
       {{ state_attr('sensor.solax_rest_cloud', 'batPower') }}
      unit_of_measurement: "W"
      device_class: energy
      state_class: measurement
      unique_id: solax_cloud_battery_use
      icon: mdi:battery

    - name: "Solax Cloud Battery Adjusted"
      state: >
        {% set ac = states('sensor.solax_cloud_inverter')|int %}
        {% set pv = states('sensor.solax_cloud_solar_panel')|int %}
        {{ (0 - ac + pv) }}
      unit_of_measurement: "W"
      device_class: energy
      state_class: measurement
      unique_id: solax_cloud_battery_adjusted
      icon: mdi:battery

    - name: "Solax Cloud Battery Use In"
      state: >
        {% set batAdj = states('sensor.solax_cloud_battery_adjusted')|int(default=0) %}
        {% set attr = state_attr('sensor.solax_rest_cloud', 'batPower') %}
        {{ batAdj if is_number(batAdj) and (batAdj|int > 0) else 0 }}
      unit_of_measurement: "W"
      unique_id: solax_cloud_battery_use_in
      icon: mdi:battery

    - name: "Solax Cloud Battery Use Out"
      state: >
        {% set batAdj = states('sensor.solax_cloud_battery_adjusted')|int(default=0) %}
        {% set attr = state_attr('sensor.solax_rest_cloud', 'batPower') %}
        {{ (0 - batAdj) if is_number(batAdj) and (batAdj|int < 0) else 0 }}
      unit_of_measurement: "W"
      unique_id: solax_cloud_battery_use_out
      icon: mdi:battery

    - name: "Solax Cloud Battery"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'soc') }}"
      unit_of_measurement: "%"
      device_class: battery
      state_class: measurement
      unique_id: solax_12
      icon: mdi:battery

    - name: "Solax Cloud Grid Power"
      state: >
        {% set attr = state_attr('sensor.solax_rest_cloud', 'feedinpower') %}
        {{ (0 - attr) if is_number(attr) else 0 }}
      unit_of_measurement: "W"
      device_class: energy
      state_class: measurement
      unique_id: solax_13
      icon: mdi:transmission-tower

    - name: "Solax Cloud Grid Power in"
      state: >
        {% set attr = state_attr('sensor.solax_rest_cloud', 'feedinpower') %}
        {{ (0 - attr) if is_number(attr) and (attr|float < 0) else 0 }}
      unit_of_measurement: "W"
      unique_id: solax_14
      icon: mdi:transmission-tower

    - name: "Solax Cloud Grid Power out"
      state: >
        {% set attr = state_attr('sensor.solax_rest_cloud', 'feedinpower') %}
        {{ attr if is_number(attr) and (attr|float > 0) else 0 }}
      unit_of_measurement: "W"
      unique_id: solax_15
      icon: mdi:transmission-tower

    - name: "Solax Cloud Energy To Grid"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'feedinenergy') }}"
      unit_of_measurement: "kWh"
      unique_id: solax_16
      icon: mdi:transmission-tower
      device_class: energy
      state_class: total_increasing

    - name: "Solax Cloud Energy From Grid"
      state: "{{ state_attr('sensor.solax_rest_cloud', 'consumeenergy') }}"
      unit_of_measurement: "kWh"
      unique_id: solax_19
      icon: mdi:transmission-tower
      device_class: energy
      state_class: total_increasing

    - name: "Solax Cloud Status"
      unique_id: solax_20
      icon: mdi:solar-power-variant
      state: >
          {% set attr = state_attr('sensor.solax_rest_cloud', 'inverterStatus') %}
          {% if attr == '100' %}Wait
          {% elif attr == '101' %}Check
          {% elif attr == '102' %}Normal
          {% elif attr == '103' %}Fault
          {% elif attr == '104' %}Permanent Fault
          {% elif attr == '105' %}Update
          {% elif attr == '106' %}EPS Check
          {% elif attr == '107' %}EPS
          {% elif attr == '108' %}Self-test
          {% elif attr == '109' %}Idle
          {% elif attr == '110' %}Standby
          {% elif attr == '111' %}Pv Wake Up Bat
          {% elif attr == '112' %}Gen Check
          {% elif attr == '113' %}Gen Run
          {% else %}unknown{% endif %}

    - name: SolaX Cloud Total Home Use
      unique_id: home use
      state: >
          {% set battery = states('sensor.solax_battery_use') %}
          {% set grid = states('sensor.solax_grid_power') %}
          {% set solar = states('sensor.solar_panel_power') %}
          {% set total = 0 - battery|float if is_number(battery) else 0 %}
          {% set total = total + (solar|float if is_number(solar) else 0) %}
          {% set total = total + (grid|float if is_number(grid) else 0) %}
          {{ total }}
      state_class: measurement
      icon: 'mdi:flash'
      unit_of_measurement: W
      device_class: power

# --- LOCAL --------------------------

### Local sensor readings ###
    # Each valid SN seems to be 10 characters
    - name: solax_local
      state: > 
            {% if state_attr('sensor.solax_rest_local', 'sn')|length == 10  %}
              {{ now().strftime("%H:%M:%S") }}
            {% else %}
              {{ (states('sensor.solax_local')) }}
            {% endif %}
      attributes: 
        sn: >-
            {% if state_attr('sensor.solax_rest_local', 'sn')|length == 10 %}
              {{ (state_attr('sensor.solax_rest_local', 'sn')) }}
            {% else %}
              {{ (state_attr('sensor.solax_local', 'sn')) }}
            {% endif %}
        ver: >-
          {% if state_attr('sensor.solax_rest_local', 'sn')|length == 10 %}
            {{ (state_attr('sensor.solax_rest_local', 'ver')) }}
          {% else %}
            {{ (state_attr('sensor.solax_local', 'ver')) }}
          {% endif %}
        type: >-
          {% if state_attr('sensor.solax_rest_local', 'sn')|length == 10 %}
            {{ (state_attr('sensor.solax_rest_local', 'type')) }}
          {% else %}
            {{ (state_attr('sensor.solax_local', 'type')) }}
          {% endif %}
        Data: >-
          {% if state_attr('sensor.solax_rest_local', 'sn')|length == 10 %}
            {{ (state_attr('sensor.solax_rest_local', 'Data')) }}
          {% else %}
            {{ (state_attr('sensor.solax_local', 'Data')) }}
          {% endif %}
        Information: >-
          {% if state_attr('sensor.solax_rest_local', 'sn')|length == 10 %}
            {{ (state_attr('sensor.solax_rest_local', 'Information')) }}
          {% else %}
            {{ (state_attr('sensor.solax_local', 'Information')) }}
          {% endif %}

### Make the settings look like sensor data by embedding the info in the Data attribute 
    - name: solax_local_settings
      state: > 
            {{ now().strftime("%H:%M:%S") }}
      attributes: 
        Data: >-
          {{ (states('sensor.solax_rest_local_settings')) }}

#### Combined Solar PV output ####
 
    - name: "Solax Local PV Output"
      unique_id: solax_local_pv_output
      state: "{{ (state_attr('sensor.solax_local', 'Data')[8] + state_attr('sensor.solax_local', 'Data')[9]) | int(default=0) }}"
      unit_of_measurement: "W"
      state_class: measurement
      device_class: "power"

### Inverter output (negative for charging battery) ####
    - name: "Solax Local AC Power"
      unique_id: solax_local_ac_power
      state: >
        {% if state_attr('sensor.solax_local', 'Data')[2] > 32767 %}{{ (state_attr('sensor.solax_local', 'Data')[2] - 65536) | int(default=0) }}
        {% else %}{{ state_attr('sensor.solax_local', 'Data')[2] | int(default=0) }}{% endif %}
      unit_of_measurement: "W"
      state_class: measurement
      device_class: "power"

### Battery current, voltage and temperature ###
    - name: "Solax Local Battery Voltage"
      unique_id: solax_local_battery_voltage
      state: "{{ state_attr('sensor.solax_local', 'Data')[14] | float / 100}}"
      unit_of_measurement: "V"
      device_class: "voltage"

    - name: "Solax Local Battery Current"
      unique_id: solax_local_battery_current
      state: >
        {% if state_attr('sensor.solax_local', 'Data')[15] > 32767 %}{{ (state_attr('sensor.solax_local', 'Data')[15] - 65536) / 100 }}
        {% else %}{{ state_attr('sensor.solax_local', 'Data')[15] / 100 }}{% endif %}
      unit_of_measurement: "A"
      device_class: "current"

    - name: "Solax Local Battery Temperature"
      unique_id: solax_local_battery_temp
      state: "{{ state_attr('sensor.solax_local', 'Data')[17] | int(default=0) }}"
      unit_of_measurement: "°C"
      device_class: "temperature"

### Battery charging/discharging power (positive for charging the battery) ###
    - name: "Solax Local Battery Power"
      unique_id: solax_local_battery_power
      state:  >
        {% if state_attr('sensor.solax_local', 'Data')[16] > 32767 %}{{ state_attr('sensor.solax_local', 'Data')[16] - 65536 }}
        {% else %}{{ state_attr('sensor.solax_local', 'Data')[16] }}{% endif %}
      unit_of_measurement: "W"
      #state_class: measurement
      device_class: "power"

    - name: "Solax Local Battery Power Adjusted"
      unique_id: solax_local_battery_power_adjusted
      state:  >
        {% set battery = states('sensor.solax_local_battery_power')|int %}
        {% set ac = states('sensor.solax_local_ac_power')|int %}
        {% set pv = states('sensor.solax_local_pv_output')|int %}
        {{ (0 - ac + pv) }}
      unit_of_measurement: "W"
      #state_class: measurement
      device_class: "power"

### Battery charge level (%)
    - name: "Solax Local Battery SoC"
      unique_id: solax_local_battery_soc
      state: "{{ state_attr('sensor.solax_local', 'Data')[18] | int(default=0) }}"
      unit_of_measurement: "%"

### Battery discharge min (%) 
    - name: "Solax Local Battery Min SoC"
      unique_id: solax_local_battery_min_soc
      state: "{{ state_attr('sensor.solax_local_settings', 'Data')[0] | int(default=0) }}"
      unit_of_measurement: "%"

### Battery charge from grid 
    - name: "Solax Local Battery Charge From Grid"
      unique_id: solax_local_battery_grid_enabled
      state: "{{ state_attr('sensor.solax_local_settings', 'Data')[1] | int(default=0) }}"
      
### Battery charge level to (%) from grid
    - name: "Solax Local Battery Charge From Grid To"
      unique_id: solax_local_battery_charge_from_grid_to
      state: "{{ state_attr('sensor.solax_local_settings', 'Data')[2] | int(default=0) }}"
      unit_of_measurement: "%"

### Estimated remaining energy ###
    - name: "Solax Local Battery Remain Energy"
      unique_id: solax_local_battery_kwh
      state: "{{ states('sensor.solax_local_battery_soc') | int(default=0) * 6.2 / 100 }}"
      unit_of_measurement: "kWh"
      device_class: "energy"

### Grid power (positive for feed-in, negative for consumption) ###
    - name: "Solax Local Grid Power"
      unique_id: solax_local_grid_power
      state:  >
        {% if state_attr('sensor.solax_local', 'Data')[32] > 32767 %}{{ (state_attr('sensor.solax_local', 'Data')[32] - 65536) }}
        {% else %}{{ state_attr('sensor.solax_local', 'Data')[32] }}{% endif %}
      unit_of_measurement: "W"
      state_class: measurement
      device_class: "power"

### Expected household load (negative not expected as it only consumes energy) ###

    - name: "Solax Local Load Power"
      unique_id: solax_local_load_power
      state: "{{ states('sensor.solax_local_ac_power')| float(default=0) - states('sensor.solax_local_grid_power') | int(default=0) }}"
      unit_of_measurement: "W"
      device_class: "power"

sensor: !include sensor.yaml

sensor powercalc_label: !include powercalc.yaml

### Commands for controlling the Solax Local inverter

rest_command:

  solax_local_charge_battery_from_grid:
    url: !secret solax_local_ip
    method: post
    payload: !secret solax_local_set_battery_level_payload

powercalc.yaml

- platform: powercalc
  entity_id: sensor.solax_local_pv_output
  name: Solar Panels V2
  fixed:
    power: "{{ states('sensor.solax_local_pv_output')| int(default=0) }}"

- platform: powercalc
  entity_id: sensor.solax_cloud_solar_panel
  name: Solax Cloud Solar Panels V1
  fixed:
    power: "{{ states('sensor.solax_cloud_solar_panel')| int(default=0) }}"

- platform: powercalc
  entity_id: sensor.solax_cloud_battery_use_in
  name: Solar Cloud Battery In V1
  fixed:
    power: "{{states('sensor.solax_cloud_battery_use_in')}}"

- platform: powercalc
  entity_id: sensor.solax_cloud_battery_use_out
  name: Solar Cloud Battery Out V1
  fixed:
    power: "{{states('sensor.solax_cloud_battery_use_out')}}"

Smart battery charging

The purpose of the below automation is to leverage cheap night-time electricity tariffs (e.g. Octopus Go) and avoid higher daytime prices. The charge targets are quite conservative in order to avoid hitting 100% charge during the day and having to send solar back to the grid - maximising self-use. Overall, the script wakes up at 23:30 every night and tries to set a new battery charge target (assuming charge from grid is enabled in the inverter settings). To calculate the appropriate value, we use tomorrow’s solar forecast. The new target is saved to an input number so that it can be easily compared with the refreshed settings. This is attempted up to 3 times to make sure the settings reflect the newly set target.

To cater for sunny days when at the start of the off-peak tariff where is remaning energy from the previous day, the bottom two automations allow for discharge down to the newly calculated target. This is to use up all left-over electricity and get the system ready for the next day. Once the charge level drops to the new charge target, the automation kicks in to stop further discharge by setting the start period for forced charge to 04:25 (same as end of forced charge window).

With these three automations the battery is in use pretty much all the time - so in addition to just capturing surplus solar, it can also intelligently use off-peak tariffs and top up the battery for less sunny days.

automations.yaml

- id: '1395837280000'
  alias: Set Solar Battery Target Level
  description: Set target battery level (charge from grid) based on anticipated solar production
  trigger:
  - platform: time
    at: "23:30:00"
  condition: []
  action:
  - repeat:
      sequence:
      - service: input_number.set_value
        data_template:
          entity_id: input_number.new_battery_target
          value: >
            {% set solar = states('sensor.energy_production_tomorrow') %}
            {% set target = 92 %} 
            {% if is_number(solar) %}
              {% set target = 50 if (solar|float > 3 and solar|float <= 4) else target %}
              {% set target = 30 if (solar|float > 4 and solar|float <= 5) else target %}
              {% set target = 25 if (solar|float > 5 and solar|float <= 6) else target %}
              {% set target = 20 if (solar|float > 7 and solar|float <= 8) else target %}
              {% set target = 15 if (solar|float > 8) else target %}
            {% endif %} 
            {{ target }}
      - service: rest_command.solax_local_set_charge_battery_from_grid
        data:
          # Max level to 92% to reduce battery wear. Minimum to 15% (10% for the min level plus 5% for early morning use until the sun comes out).
          level: >
            {{ states('input_number.new_battery_target') }}
      - delay:
          hours: 0
          minutes: 0
          seconds: 10
          milliseconds: 0
      - service: homeassistant.update_entity
        entity_id: sensor.solax_rest_local_settings

      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('sensor.solax_local_battery_charge_from_grid_to')|int == states('input_number.new_battery_target')|int or
                repeat.index == 3 }}
  mode: single

# Always reset to the same state - i.e. no off-peak forced charging overnight
- id: "1143938438939"
  alias: Battery - Disable forced charge
  description: "Allows for discharge if too much energy stored from previous day (default state)"
  trigger:
  - platform: time
    at: "23:40:00"  
  - platform: time
    at: "23:50:00"
  action:
  - repeat:
      sequence:
      - service: rest_command.solax_local_set_forced_charge_start
        data:
          value: >
            {{ 4 + 25 * 256 }}
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
          milliseconds: 0
      - service: homeassistant.update_entity
        entity_id: sensor.solax_rest_local_settings

      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('sensor.solax_local_battery_setting_start_charge') == '04:25' or repeat.index == 3 }}
  mode: single

# Detect if battery needs topping up within the off-peak tariff or if discharge should stop and the remainder power be left for peak usage 
# (+4 is how much the inverter overshoots the target charge, +4 is buffer in case of fast discharge)
- id: "1029376657476"
  alias: Battery - Enable forced charge
  description: "Stops allowed discharge and allows for charging to target"
  trigger:
  - platform: time_pattern
    minutes: "/1"
  condition: 
  - condition: time
    after: "00:25:00"
    before: "04:20:00"
  - condition: template
    value_template: >-
      {{ states('sensor.solax_local_battery_soc')|int <= states('sensor.solax_local_battery_charge_from_grid_to')|int + 8 }}
  action:
  - repeat:
      sequence:
      - service: rest_command.solax_local_set_forced_charge_start
        data:
          value: >
            {{ 0 + 35 * 256 }}            
      - delay:
          hours: 0
          minutes: 0
          seconds: 15
          milliseconds: 0
      - service: homeassistant.update_entity
        entity_id: sensor.solax_rest_local_settings

      until:
      - condition: template
        # Try up to 3 times if the updated setting doen't reflect the target
        value_template: >-
            {{ states('sensor.solax_local_battery_setting_start_charge') == '00:35' or repeat.index == 3 }}
  mode: single

And here is what it looks like in practice (top - actual solar output, bottom - battery charge level in %):

Smart electric heater

This automation is about prioritising the battery charging from solar to almost full, while gradually using excess solar power and minimise grid export. This uses a dumb 2kW electric heater (on/off) and a smart plug rated to 13A. For details please see Solar-powered smart convector heater.

Smart immersion heating (hot water)

I’ve written a separate article covering hot water immersion heating. Not only for excess solar, but one that should provide a complete hot water heating solution (night-time, solar and evening top-up). Check it out at Smart immersion heating for domestic hot water.

Octopus Saving Sessions

For details on trying to automate forced export during the Octopus Saving Sessions please see Automated Octopus Saving Sessions with Solax X1 Hybrid G4 page.

Summary

I’m defenitely not an expect in Home Assistant, but I hope this tested configuration proves useful to a few people. While I’m confident with the monitoring aspects, I definitely have more testing and improvements to make on the control & automation side.

Any suggestions on what to improve will be highly appreciated. Happy monitoring!

11 Likes

Great stuff - will merge into my local implementation shortly

My next step is to control the settings (e.g. change the charge battery to ) either via Solax Cloud or ideally through the local connection (any ideas on how to eavesdrop on the solax app in local connection mode? :slight_smile: ).

I am happy to collaborate on this.

First, I noticed you can change the charge battery to setting from the cloud API. So I was planning to use the Google Chrome devs tools to see what the communication looks like.

Then I plan to try an emulate it with bash/curl/python.
Once there, then figure out a way to integrate into HA.

This apporach may get tied into the cloud version, and not use the local interface, but will not know that for sure until we’ve investigated.

Not my top priority, so may be a week or two until I get to investigate.

Health Warning. This approach will be using undocumented APIs, with unknown side effects, so use at you own risk.

1 Like

You are a genius!!
Been looking for the local access, but everyone says inverter can only accept one connection (Pocket WIFI) and have to reverse proxy

Have local connection that works now :rofl:.
I am not a coder or fully understand what I am copying and pasting into .yaml files.
Is there any way of not having the massive sensor coding in the configuration file. Not good for my OCD :grinning:

1 Like

Thanks @74Quickie74. I’m sure you could shorten those yaml files by removing sensons you don’t need (e.g. battery or all the cloud ones), but other than that, not sure what else. Only been using HA for a few weeks though…

@ColinRobbins many thanks for the offer - would love to work together on this! I did make some decent progress on both reading the settings and changing them via the local API - see the updated article. There is more work for sure, but I though it’s better to publish earlier than later :wink:

@kamilb Great stuff. I have it working locally, and have sucessfully modified the battery charge level.

Why in your automation do you call the REST command twice ?

I have made a few modifications in my environment.
I notice the local API occassionally returns an error, so I’ve added a little error handling to each sensor…

  - name: "Solax Solar Panel"
    unit_of_measurement: "W"
    state: >
        {% if state_attr('sensor.solax_local', 'Data') != None %}
          {{ state_attr('sensor.solax_local', 'Data')[8]|int }}
        {% else %} unknown {% endif %}
    device_class: energy
    state_class: measurement
    unique_id: solax_8
    icon: mdi:solar-power-variant

I also added a few extra sensors…

  - name: "Solax AC Voltage"
    state: >
      {% if state_attr('sensor.solax_local', 'Data') != None %}
        {{ state_attr('sensor.solax_local', 'Data')[0] |int/10}}
      {% else %} unknown {% endif %}
    unit_of_measurement: "V"
    device_class: "voltage"
    unique_id: solax_100

  - name: "Solax AC Frequency"
    state: >
      {% if state_attr('sensor.solax_local', 'Data') != None %}
        {{ state_attr('sensor.solax_local', 'Data')[3] |int/100}}
      {% else %} unknown {% endif %}
    unit_of_measurement: "Hz"
    device_class: "frequency"
    unique_id: solax_101

What would the rest magic to replace

solax_local_set_battery_level_payload: 'optType=setReg&pwd=REG_NO&data={"num":1,"Data":[{"reg":31,"val":"{{ level }}"}]}'

be, if I wanted to set the minimum SOC instead?
(I want to set this high, when charging my EV, to stop the battery draining into my EV)

I’ve used a simplified automation, with all the logic in one place.

alias: Set Solar Battery Level
description: Set level based on anticipated production
trigger:
  - platform: time
    at: "23:00:00"
condition: []
action:
  - service: rest_command.solax_local_charge_battery_from_grid
    data:
      level: >
        {% set solar = states('sensor.energy_production_tomorrow') %} 
        {% set target = 100 %} 
        {% if is_number(solar) %}
          {% set target = 80 if (solar|int > 3 and solar|int <= 5) else target %}
          {% set target = 60 if (solar|int > 5 and solar|int <= 7) else target %}
          {% set target = 30 if (solar|int > 7 and solar|int <= 10) else target %}
          {% set target = 10 if (solar|int > 10) else target %}
        {% endif %} 
        {{ target }}
mode: single

Great suggestions @ColinRobbins.

  1. Regarding setting the min SOC, see below (tested with Developer Tools → Services → Call Service). However, I get the impression that when in forced charge period, the battery is not being used for powering the loads, even if the target is reached.
solax_local_set_battery_min_soc_payload: 'optType=setReg&pwd=REG_NO&data={"num":1,"Data":[{"reg":29,"val":"{{ level }}"}]}'
  1. I really like your simplified automation - will test and update the page. Maybe it’s best to avoid charging to 100% to avoid excessive battery wear - I will try with 95%. For now also trying minimum of 15% to allow for morning usage, even if the forecast is good. Eventually would like to calculate the number of hours (say between 0430 and sunrise, and estimate the min % from that).

  2. The reason for repeating the REST command is that it seems to me the call sometimes fails silently - not sure if this is due to HA or Solax. Same seems to happen on the command line with curl, so I suspect something on the inverter side. I have no idea how to handle these errors so blindly repeating the call :slight_smile: Would be nice to then read the settings and verify if it changed, and only repeat if not.

I have found updating the sensor.solax_rest_local_settings,
every 10 seconds was causing issues with timeouts etc.
So I backed it off to 3600 second updates.

Then in my automation when I set the value, i wait 5 seconds then call

service: homeassistant.update_entity
data: {}
target:
  entity_id: sensor.solax_rest_local_settings

@kamilb I have this working, thanks for sharing!

However, I don’t think I can set up correctly the Energy Dashboard with the current setup. How do you have it done?

It doesn’t really let me select any of the Local ones which are accurate.

Thanks @ColinRobbins - yes, changing settings refresh to 3600 seconds seems very sensible. I also added the refresh step in automation as you suggested, but also a loop to ensure the new settings reflect the new target. I still see it occasionally going the second time, so three is just in case. I’ve also tried to fine-tune the forecast-based target, to avoid 100% charge with solar. This will certainly require fine tuning depending on everyone’s usage. Next on my list:

  • Update the base target depending on number of hours between end of charging and sunrise
  • Another automation to put an electric heater on/off depending on feed-in energy
  • Look at putting the immersion heater (hot water) on/off depending on solar forecast & solar pv output (although for that I first need a switch - maybe Sonoff Powr3?

Are any of these of any interest?

Hi @ALaguna. So far I’ve only configured the energy dashboard with the cloud sensors:

It should be possible to do it of the local readings, but I’m not sure how reading frequency (every 5s) would affect that, hence sticking with the cloud ones for now.

BTW - don’t use the yield value from solax - it’s wrong in case you charge battery from grid. Also, the total yield recently jumped from 500kWh to over 1MWh!!! I just don’t trust it any more…

Better use the PV output with powercalc, altough that will be higher that actual field.

1 Like

Thanks for getting back!

I’m not sure the numbers from the cloud make sense. It’s night right now and I don’t have any battery or solar production:

"feedinpower": -1537.0,
"feedinenergy": 206.17,
"consumeenergy": 553.68,

Do you happen to know how does the local array work? I can’t really make sense of it, specially when it comes to Production, Feed In and Load Generator

{
  "sn": "SXXXXXXXX",
  "ver": "3.003.02",
  "type": 4,
  "Data": [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3986, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 287, 0, 0, 0, 0, 0, 0, 64384, 65535, 20617, 0, 55374, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
  ],
  "Information": [
    5, 4, "XBXXXXXXXXXXX", 8, 0, 0, 1.35, 0, 0, 1
  ]
}

Here is a link to some information regarding the data in the cloud api https://www.eu.solaxcloud.com/phoebus/resource/files/userGuide/Solax_API_for_End-user_V1.0.pdf
feedinenergy should be total energy (kWh) put in to the grid and consume energy is total energy consumed from the grid and has nothing to do with current power. Feedinpower is negative because you are not feeding energy in to the grid, but taking from.
Regarding the data, @kamilb has a nice google sheets document in his first post ( X1 Hybrid G4 - Google Sheets ) and you can also check Solax X3-Hybrid G4 Wifi Firmware Solax module fw version 3.003.02 Home Assistant Integration (also linked in the first post) for more info about what the different values in the array denotes.

Just started with this myself the other day, thanks a lot for the information @kamilb !
I’m going to take a deep dive into the settings data any day now, to see if I can help with some decoding. Especially regarding charge&discharge hours.

2 Likes

When I execute the curl command the response I am getting is “curl: (52) Empty reply from server”.

*   Trying 192.168.178.109:80...
* Connected to 192.168.178.109 (192.168.178.109) port 80 (#0)
> POST / HTTP/1.1
> Host: 192.168.178.109
> User-Agent: curl/7.83.1
> Accept: */*
> Content-Length: 39
> Content-Type: application/x-www-form-urlencoded
>
* Empty reply from server
* Closing connection 0
curl: (52) Empty reply from server

My Solax Pocket WiFi stick is a V2 version with firmware version 2.033.20. The inverter is a Solax X1 Mini (X1-2.0-S-D).

Sorry @witterholt, won’t be able to help with your firmware & inverter version. Have you reached out to Solax for a firmware update?

No, I am using the reverse proxy method now.

1 Like

Hi @ALaguna ,

I am just setting my own Solax string inverter up and trying the same thing. I started off by modifying a copy of the cloud total yield template:

#### Solar AC Total Yield ###

    - name: "Solax Local AC Total Yield"
      unique_id: solax_local_ac_total_yield
      state: "{{ (state_attr('sensor.solax_local', 'Data')[11]) | float(default=0) / 10 }}"
      unit_of_measurement: "kWh"
      state_class: "total_increasing"
      device_class: "energy"

However, at the start of the day, it seemed to have a glitch where Total Yield would be its correct value, then drop to zero, then go back up to its correct value. The dashboard would then count the increase as generation, which would be wrong.

I am now trying 2 different definitions out:

  1. Continue to use the Total Yield data, but add a check for a 0 result and re-use the previous value, to get past the glitch
  2. Use the Daily Yield data. However, this drops to zero at sun down and I’m not sure how the dashboard will react to that.

The code I am using for these 2 new templates is:

#### Solar AC Total Yield - Error Check 0 Value ###

    - name: "Solax Dashboard AC Total Yield"
      unique_id: solax_dashboard_ac_total_yield
      state:  >
        {% if state_attr('sensor.solax_local', 'Data')[11] == 0 %}{{ states('sensor.solax_dashboard_ac_daily_yield') }}
        {% else %}{{ (state_attr('sensor.solax_local', 'Data')[11]) | float(default=0) / 10 }}{% endif %}
      unit_of_measurement: "kWh"
      state_class: "total_increasing"
      device_class: "energy"

#### Solar AC Dashboard Daily Yield ###

    - name: "Solax Dashboard AC Daily Yield"
      unique_id: solax_dashboard_ac_daily_yield
      state: "{{ (state_attr('sensor.solax_local', 'Data')[13]) | float(default=0) / 10 }}"
      unit_of_measurement: "kWh"
      state_class: "total_increasing"
      device_class: "energy"

I updated these just now, so I’m trying these for the first time tomorrow. Feel free to give it a go too.

Hey! Thanks for answering!

What’s your inverter_type the Data makes me a bit nervous