My-PV AC-THOR

I’ve implemented My-PV AC-THOR to my Home Assistant.

Dashboard:

type: entities
entities:
  - entity: sensor.ac_thor_leistung
  - entity: sensor.ac_thor_meter
  - entity: sensor.ac_thor_temperatur

configuration.yaml:
modbus: !include modbus.yaml

modbus.yaml:

- name: AC-THOR
  type: tcp
  host: ac-thor.local
  port: 502
  sensors:
  - name: "AC_THOR Meter"
    unique_id: ac_thor_meter
    address: 1069
    unit_of_measurement: W
    state_class: total_increasing
  - name: "AC_THOR Leistung"
    unique_id: ac_thor_power
    address: 1000
    unit_of_measurement: W
    state_class: total_increasing
  - name: "AC_THOR Temperatur"
    unique_id: ac_thor_temperature
    address: 1001
    scale: 0.1
    unit_of_measurement: "°C"

AC-THOR would need a ModBus Server where it could read Meter + battery charging" values from.
Unfortunately, I have a Battery system, which does not speak ModBus (Tesla Powerwall), but I can read the values through REST-API from the Powerwall.

I would need a ModBus Server as an add-on for the hass-os, where I can set-up some
on the Homeassistant Server, which emulates some hold registers by looking them up from corresponding hass sensor values. Does something like this exist?

I’ve already wrote a prototype (without hass integration), which already emulates modbus registers 40206 - 40210 - unfortunately only hardcoded - please keep in mind, this is a brutal hack) of an emulated “SolarEdge Manual” backend system, which uses values from the Powerwall values (which includes metering of the capacity from photovoltaik versus grid). Integration within Hass would be nice, but I’m not experienced, how to write a module.

#!/usr/bin/env python3
# pyModbusTCP pyPowerWall Modbus-Frontend für Tesla Powerwall
# using example https://pymodbustcp.readthedocs.io/en/latest/examples/server_virtual_data.html

from pypowerwall import Powerwall
from pyModbusTCP.server import ModbusServer, DataBank, DataHandler
from pyModbusTCP.constants import EXP_ILLEGAL_FUNCTION, EXP_NONE, EXP_DATA_ADDRESS
from time import sleep
from random import uniform
from datetime import datetime

# some const
ALLOW_R_L = ['127.0.0.1', '192.168.1.xxxx_ip_of_ac_thor']
PW_IP = "192.168.1.xxxx_ip_of_powerwall"
PW_LO = "[email protected]"; PW_PW = "powerwall_customer_pw"
PW_TZ = "Europe/Berlin"

class MyDataHandler(DataHandler):
    def read_coils(self, address, count, srv_info):
        if srv_info.client.address in ALLOW_R_L:
            print ("Coil", address, count)
            return super().read_coils(address, count, srv_info)
        else:
            return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)

    def read_d_inputs(self, address, count, srv_info):
        if srv_info.client.address in ALLOW_R_L:
            print ("Inputs", address, count)
            return super().read_d_inputs(address, count, srv_info)
        else:
            return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)

    def read_h_regs(self, address, count, srv_info):
        if srv_info.client.address in ALLOW_R_L:
            if (address == 40206 and count == 5):
                grid = -pw.grid()
                if grid > 2000:
                    grid = grid - 2000
                else:
                    grid = 0
                print ("Handle", grid)
                return DataHandler.Return(exp_code=EXP_NONE,
                        data=[grid, 0, 0, 0, 0])
            else:
                print ("Handle", address, count)
                return super().read_h_regs(address, count, srv_info)
        else:
            return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)

    def read_i_regs(self, address, count, srv_info):
        if srv_info.client.address in ALLOW_R_L:
            print ("I_Regs", address, count)
            return super().read_i_regs(address, count, srv_info)
        else:
            return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)

    def write_coils(self, address, bits_l, srv_info):
        if srv_info.client.address in ALLOW_W_L:
            return super().write_coils(address, bits_l, srv_info)
        else:
            return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)

    def write_h_regs(self, address, words_l, srv_info):
        if srv_info.client.address in ALLOW_W_L:
            return super().write_h_regs(address, words_l, srv_info)
        else:
            return DataHandler.Return(exp_code=EXP_ILLEGAL_FUNCTION)

if __name__ == '__main__':

    pw = Powerwall(PW_IP, PW_PW, PW_LO, PW_TZ)
    server = ModbusServer("0.0.0.0", 502, no_block=True, data_hdl=MyDataHandler())
    try:
        print("Start server...")
        server.start()
        print("Server is online")
        state = [0]
        while True:
            sleep(60)
    except:
        print("Shutdown server ...")
        server.stop()
        print("Server is offline")

Answer to myself:

I’ve reimplemented with http (instead of modbus), because you can only have one MODBUS connection to the AC-THOR and http has more chance to connect to the AC-THOR for one session reading values, and the other session writing the power value at the same time.

Here’s my actual code (please use value_json.power_act instead of value_json.power_ac9 for 3K version of AC-THOR):

Read from AC-THOR:

# configuration.yaml
sensor:
  - platform: rest
    name: "AC_THOR Leistung"
    unique_id: ac_thor_leistung
    resource: http://192.168.1.x/data.jsn
    scan_interval: 30
    value_template: '{{ value_json.power_ac9 }}'
    unit_of_measurement: W
    state_class: total_increasing

  - platform: rest
    name: "AC_THOR Temperatur"
    unique_id: ac_thor_temperature
    resource: http://192.168.1.x/data.jsn
    value_template: '{{ value_json.temp1 / 10 | round(1) }}'
    unit_of_measurement: "°C"
    state_class: total_increasing

Write to AC-THOR:

# configuration.yaml
rest_command:
  update_acthor_power:
    url: 'http://192.168.1.x/control.html?power={{ power }}'

# automations.yaml
- alias: AC-THOR-Update
  trigger:
  - platform: state
    entity_id:
    - sensor.strom_leistung
  condition: []
  action:
  - service: rest_command.update_acthor_power
    data:
      power: sensor.strom_leistung
  mode: single

yes that exist, works great!

For setting the state on my Thor9s, I needed to change the URI. For example to enable/disable heating:

# configuration.yaml
rest_command:
  update_acthor_devmode:
    url: 'http://192.168.1.x/setup.jsn?devmode={{ mode }}'

Finally used the shell_command integration to change the states and the rest integration for status updates

#configuration.yaml
shell_command:
  thor_9s_update_cmd: "curl 'http://192.168.1.x/setup.jsn?{{param}}={{value}}'"

rest:
  - resource: "http://192.168.10.1.x/data.jsn"
    scan_interval: 10
    sensor:
      - name: "thor9s_power_ac9"
        value_template: "{{ value_json.power_ac9 }}"
      - name: "thor9s_temp1"
        value_template: "{{ value_json.temp1 / 10 | round(1) }}"
      - name: "thor9s_boostactive"
        value_template: "{{ value_json.boostactive }}"
      - name: "thor9s_load_state"
        value_template: "{{ value_json.load_state }}"    


#automation.yaml    example for turning the device off
...
sequence:
   - service: shell_command.thor_9s_update_cmd
     data:
       param: devmode
       value: 0
...

Hi, I have made AC-Thor (3kW version) connection working with Modbus and LAN with Homewizard P1 meter.

In my configuration.yalm

rest_command:
  update_acthor_power:
    url: 'http://192.168.X.X/control.html?pid_power={{ power }}' # edit IP
  update_acthor_devmode:
    url: 'http://192.168.X.X/setup.jsn?devmode={{ mode }}' # edit IP

shell_command:
  thor_9s_update_cmd: "curl 'http://192.168.x.x/setup.jsn?{{param}}={{value}}'" # edit IP

In modbus.yalm

- name: AC-THOR
  type: tcp
  host: 192.168.x.x #edit ip
  port: 502
  sensors:
  - name: "AC_THOR Meter modbus"
    unique_id: ac_thor_meter
    address: 1069
    unit_of_measurement: W
    state_class: total_increasing
  - name: "AC_THOR Leistung modbus"
    unique_id: ac_thor_power
    address: 1000
    unit_of_measurement: W
    state_class: total_increasing
  - name: "AC_THOR Temperatuur modbus"
    unique_id: ac_thor_temperature
    address: 1001
    scale: 0.1

Automation:

alias: AC Thor push values
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.p1_meter_5c2faf055da6_active_power
condition: []
action:
  - service: script.ac_thor_push_values_p1_meter
    data: {}
mode: single

Script:

alias: AC thor push values P1 meter
sequence:
  - service: modbus.write_register
    data_template:
      hub: AC-THOR
      address: 1000
      value: "{{ states('sensor.P1_active_power_inverted') | int}}"
    enabled: true
mode: single

Automation that sets the value to send to address 1000 of ACThor.
Homewizard is not compatible P1 meter, so it send the wrong grid feed in data to the AC Thor. AC-Thor needs a negative value to start working. P1 meter Homewizard sets a positive value for grid feed-in. So I invert the value by multiplying with -1. Then I add the value of what the resistor of ACThor is using, bc otherwise you get an oscillating value. You can add or substract a fixed number to get closer to 0 grid feed-in.

alias: set invert power
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.p1_meter_5c2faf055da6_active_power
condition: []
action:
  - service: input_number.set_value
    data:
      value: >-
        {{((states('sensor.p1_meter_5c2faf055da6_active_power')|float)|round(3)*(-1))+(states('sensor.ac_thor_leistung_2')|float)}}
    target:
      entity_id: input_number.temp_inverted_power
mode: single

This input_number has to be translated in a sensor using template.yaml

sensor:
    - name: "P1_active_power_inverted"
    state: '{{ states.input_number.temp_inverted_power.state | round(0) }}'

For the value of the resistor I use a LAN sensor:
Sensor.yalm:

- platform: rest
  name: "AC_THOR Leistung LAN"
  unique_id: ac_thor_leistung
  resource: http://192.168.x.x/data.jsn #edit ip
  scan_interval: 2
  value_template: '{{ value_json.power_act }}'
  unit_of_measurement: W
  state_class: total_increasing

- platform: rest
  name: "AC_THOR Temperatuur LAN"
  unique_id: ac_thor_temperature
  resource: http://192.168.x.x/data.jsn # edit IP
  value_template: '{{ value_json.temp1 / 10 | round(1) }}'
  unit_of_measurement: "°C"
  state_class: total_increasing
  scan_interval: 30

To activate the ACThor I use this script:

alias: AC Thor enable
sequence:
  - service: shell_command.thor_9s_update_cmd
    data:
      param: devmode
      value: 1
    enabled: true

Disable:

alias: AC Thor disable
sequence:
  - service: shell_command.thor_9s_update_cmd
    data:
      param: devmode
      value: 0
    enabled: true

Here is a graph how the resistance if following grid feed-in.

Is there a possibility to switch the acthor on/off via http command ? Got the elwa2

I‘m useing this integration .

@siku2

No one an idea?

this gives me a 404. Does this still work on your end and any thoughts why it isn’t working on mine?