Access Studer-Innotec Xtender PV system using local communications

Problem statement:
Want the ability to access Studer performance data from Home Assistant (HA), and change Studer settings from within HA. Having this ability would allow a more customized optimization of the PV use through the automations that are possible in HA. Studer gives the ability to monitor and control their PV systems both locally through a RCC or by web interface through their portal. Through the RESTful integration, HA can access the Studer portal to read performance data, and probably also make changes to the Studer settings, but there are some problems with this approach. The portal communication often breaks down resulting in data being inaccessible for significant periods of time. The portal only gives access to change the settings in the hardware’s flash memory and doesn’t have the option to limit changes to RAM. Studer says that their flash memory is only rated for about 1000 write cycles, so repeated writes to flash will eventually wear it out.

There are other alternatives. In Github, there are a couple of python libraries that can used to access Studer performance data from Home Assistant, and change Studer settings through local communication which avoids the Studer portal. The one that I have chosen to use is GitHub - zocker-160/xcom-protocol: Python library implementing Studer-Innotec Xcom protocol used by Xcom-232i and Xcom-LAN. I chose this one because it seems to be straight forward, and gives the option to change Studer settings in RAM only so that the flash memory isn’t worn out. This library offers 3 communication modes:
1. Local serial which is probably the most reliable and highest performance option, but requires purchasing and installing additional hardware (Xcom-485i) and wiring.
2. UDP over the existing local area network using the Xcom-LAN which most installations probably already have. I haven’t done much testing with this method, but it is probably very reliable, and reasonably fast. But using this method requires that the Moxa serial server that is used in the Xcom-LAN be reconfigured to UDP which means that the system will no longer be accessible from the portal.
3. The last method, which is a recent addition, and which I am using is with the Moxa in TCP client mode so that it can maintain communication with the Studer portal. With this method, I am able to read the Studer data about every 6 seconds while remaining connected to the portal. There are occasional short drops in communication that could be due to collisions with the portal data requests, but it has been much more reliable than using the RESTful integration and I am happy with it so far.

Summary of steps:
I. Choose a location to run the python library and script.
II. In the Moxa list of TCP servers, add the ip address of the machine that will run the python library and script.
III. From a command prompt on the machine where you installed the library, enter library calls to confirm that you can read Studer data. Do the same with writing some Studer setting changes.
IV. Add an account in HA that the python script will use for accessing HA. You will also need to create a long lived token in this account to provide the credentials to the script.
V. Test using a script to write to HA the values that were read from the Studer.
VI. Test using a script to read values from HA and write those values to the Studer.

I. Choose a location to run the python library and script.  You can start off running it on your PC to test it out, then later install it on the same machine that runs your HA installation, or install where ever it is most convenient.  
    1. Install the python library.
    2. Get the ip address of this machine.  You can find it in your router’s list of network active DHCP leases with a host name of that machine’s name, or you can run a network scanner app on your phone to find the address.  Save this address to enter in step 2.d.
    3. Follow the installation instructions at https://github.com/zocker-160/xcom-protocol to install the xcom-protocol library on your machine.
        a. Click on the green Code button, then download ZIP button to download the xcom-protocol-master.zip zipfile.  Copy this zip file to folder where you will run the python script from.
        b. Open a terminal window on the machine where the library will be installed and navigate to the folder where you will run the python script from.
        c. To install the library, at the terminal command prompt, type: pip install xcom-protocol-master.zip

II. Add machine’s ip address to Moxa’s list of TCP servers
    1. Get the ip address of your Moxa serial server from your router’s list of network active DHCP or you can run a network scanner app.
    2. Log into the Moxa by placing its ip address into a web browser.  The default user name is “admin”, and the default password is “xcomlan”.  Click continue button to access the Moxa menu.
    3. In the Moxa menu, click Operating Settings, then Port 1.
    4. Enter the ip address of the machine that will run python into an empty Destination IP address field, and 4001 in the Port field.  Click the submit button.
    5. On the next page, click the save/restart button.

III. Test the library installation, from a command prompt on the machine where you installed the library, enter library calls to test the installation:
    1. Start the python interpreter by typing “python3” at the command prompt.
    2. Import the library functions by entering the following at the python command prompt:
from xcom_proto import XcomP as param
from xcom_proto import XcomC
from xcom_proto import XcomLANTCP
    3. Read tests:
        a. To test reads, copy/paste the following block of commands to the command prompt to make a test read from your inverter (make sure to keep the 4 space indentation after the “with” statement for the getValue commands, and no indentation for the print commands):
with XcomLANTCP(port=4001) as xcom:
    soc = xcom.getValue(param.BATT_SOC)  # Make sure this line is indented 4 spaces
    gridpower = xcom.getValue(param.AC_POWER_IN)  # Make sure this line is indented 4 spaces
    houseload = xcom.getValue(param.AC_POWER_OUT)  # Make sure this line is indented 4 spaces

Wait for the “>>>” python command prompt to return. You my need to add a couple of carriage returns for it to come back.

print('soc = ',soc, '%')	     
print('gridpower = ',gridpower, 'kW')
print('houseload = ',houseload, 'kW')
    If everything is correct, you should see a print to screen of three inverter data points.
    4. Write tests to inverter RAM:
        a. First confirm that the inverter Smart Boost Limit (parameter 1607) is set to 100.0%.  (Note: writes to RAM cannot be read back, so to confirm that the write was successful, you will need to observe system behavior.)
        b. Then, at a time when the solar power is too low to meet the power needs of the house,  and  the State of Charge Level for Backup (parameter 6062), you should see power flowing from the battery to the house.  
        c. Copy/paste the following block of commands to the command prompt to make a test write to your inverter (make sure to keep the 4 space indentation after the “with” statement):
        d.
with XcomLANTCP(port=4001) as xcom:
    xcom.setValue(param.SMART_BOOST_LIMIT, 0.0)  # Make sure this line is indented 4 spaces   
        e. If everything is correct, you should see power flow from the battery stop, and the house power will instead be supplied by the grid.  Setting the Smart Boost Limit to 0, essentially makes your system behave like a grid tied system without battery backup which can be useful to reduce battery wear or to conserve battery charge for use during a high tariff period.
        f. To return the system back to the prior condition, Copy/paste the following block of commands to the command prompt:
with XcomLANTCP(port=4001) as xcom:
    xcom.setValue(param.SMART_BOOST_LIMIT, 100.0) # Make sure this line is indented 4 spaces
    5. Use the measurements python test script to perform reads.
        a. Copy the file measurements.py to the folder that the commend prompt is open to.
        b. Start python if its not already running by typing python3 at the command prompt.
        c. At the command prompt, type: measurements.py 10 3
        d. The number 10 in the argument list is the number of times to execute the loop in the script, and the number 3 is the delay to wait after each execution of the loop.
        e. If everything is correct, you should see a csv file in the folder that the script was run from.  The csv file can be opened as a spreadsheet and have 10 rows of data.  If there were errors in running the script, you may need to comment out lines that refer to hardware that is not in your system such as a second Vario String, by placing a “#” at the beginning of those lines.

IV. Add an account in HA that the python script will use for accessing HA.
    1. Add and configure account.
        a. From an HA administrative user account go to Settings, People, Users.
        b. Click the add user button in the bottom, right of the screen.
        c. Assign a Display Name, User Name, password, select Can only log in from the local network, and also select Administrator.
    2. Create a token
        a. Log into the newly created account.
        b. Click on the account properties button in the lower right screen.
        c. Scroll to the bottom of the page and click the Create Token link.
        d. Give a name to the token, and copy the token.
    3. Copy the token and HA ip address to the script.
        a. Open the script ha_xcom.py and copy the newly created token to the line 
HOME_ASS_TOKEN = "<your token>" # replacing <your token> with the token. Make sure to leave the quotes in place.
        b. While you have the script open, also copy the ip address of your HA installation to the line
HOME_ASS_URL = "http://<ip>:8123/api/states/"  # replacing <ip> with your HA installation ip address.  Make sure to leave the quotes in place.
V. Test using the ha_xcom001.py script to read values from the Studer and write those values to HA.
    1. Read values from the Studer and write those values to HA.
        a. Copy the file ha_xcom001.py to the folder that the commend prompt is open to.
        b. Start python if its not already running by typing python3 at the command prompt.
        c. At the command prompt, type: ha_xcom001.py -1 3
        d. The number -1 in the argument list tell the loop in the script to execute without stopping, and the number 3 is the delay to wait after each execution of the loop.
        e. If everything is correct, you should see in the HA Overview dashboard the creation of several new sensors that begin with “scom” and they should have live data from your Studer system that updates about every 6 seconds.

VI. Test using the ha_xcom001.py script to read values from HA and write those values to the Studer.
    1. Add numeric input helpers in HA to create the following entities
        a. input_number.studer_python_script_loop_delay, minimum 0, maximum 30
        b. input_number.studer_smart_boost_limit, minimum 0, maximum 100
        c. input_number.studer_soc_level_for_backup, minimum 66, maximum 4 (the maximum and minimum limits you choose will depend on your system configuration)
    2. In HA , add templates to the configuration.yaml file or a yaml file that is included in it to create new sensors for the script to read.
- name: "HA SCOM Studer Python Script Loop Delay"
  unique_id: "ha_studer_python_script_loop_delay"
  unit_of_measurement: "s"
  device_class: "duration"
  state_class: "measurement"
  state: >-
    {% set delay_time = states('input_number.studer_python_script_loop_delay' )|float() %}
    {{ delay_time }}

             
- name: "HA SCOM Studer Smart Boost Limit"
  unique_id: "ha_studer_smart_boost_limit"
  unit_of_measurement: "%"
  device_class: "battery"
  state_class: "measurement"
  state: >-
    {% set smart_boost = states('input_number.studer_smart_boost_limit' )|float() %}
    {{ smart_boost }}
             
- name: "HA SCOM Studer SOC Level for Backup"
  unique_id: "ha_studer_soc_level_for_backup"
  unit_of_measurement: "%"
  device_class: "battery"
  state_class: "measurement"
  state: >-
    {% set soc_level_for_backup = states('input_number.studer_soc_level_for_backup' )|float() %}
    {% set soc_level_for_grid_feeding = states('sensor.scom_soc_level_for_grid_feeding' )|float() %}
    {% if soc_level_for_backup > soc_level_for_grid_feeding %}
      {{ soc_level_for_grid_feeding }}      
    {% else %}
      {{ soc_level_for_backup }}
    {% endif %}
    3. Uncomment lines 205 to 223 in the ha_xcom001.py to allow it to read the newly created sensors.
    4. Run the ha_xcom001.py script, and you should see the Studer system respond to changes in the HA helper input values.

Import the library functions

from xcom_proto import XcomP as param
from xcom_proto import XcomC
from xcom_proto import XcomLANTCP

Test reads from python command prompt with print to screen of results:

with XcomLANTCP(port=4001) as xcom:
    soc = xcom.getValue(param.BATT_SOC)
    gridpower = xcom.getValue(param.AC_POWER_IN) 
    houseload = xcom.getValue(param.AC_POWER_OUT) 

Then fit the return key to get back the python command prompt “>>>”

Now enter the print statements:

print('soc = ',soc, '%')
print('gridpower = ',gridpower, 'kW')
print('houseload = ',houseload, 'kW')

Test write to Studer inverter:

with XcomLANTCP(port=4001) as xcom:
    xcom.setValue(param.SMART_BOOST_LIMIT, 0.0)

Script to call the library function to read Studer data and write to a csv file:

import sys
import datetime
import uuid

# Your XcomLANTCP and other imports go here
from xcom_proto import XcomP as param
from xcom_proto import XcomC
from xcom_proto import XcomLANTCP


def main():
    num_measurements = int(sys.argv[1]) if len(sys.argv) > 1 else 1

    # Generate a unique identifier
    unique_id = str(uuid.uuid4())

    with open(f'measurement_results_{unique_id}.txt', 'w') as file:
        file.write(f'Date Created: {datetime.datetime.now()}\n')
        file.write(f'Unique ID: {unique_id}\n\n')

        with XcomLANTCP(port=4001) as xcom:
            for _ in range(num_measurements):
                battCharge = xcom.getValueByID(7007, XcomC.TYPE_FLOAT)
                batt_discharge = xcom.getValueByID(7008, XcomC.TYPE_FLOAT)
                battpower = xcom.getValueByID(7003, XcomC.TYPE_FLOAT)
                soc = xcom.getValue(param.BATT_SOC)
                batttemp = xcom.getValue(param.BATT_TEMP)
                battery_voltage = xcom.getValue(param.BATT_VOLTAGE)
                grid_prod = xcom.getValue(param.AC_ENERGY_IN_CURR_DAY)
                gridpower = xcom.getValue(param.AC_POWER_IN)
                houseload = xcom.getValue(param.AC_POWER_OUT)
                solarpower = xcom.getValueByID(15010, XcomC.TYPE_FLOAT)
                solarpower1 = xcom.getValueByID(15011, XcomC.TYPE_FLOAT)
                solarpower2 = xcom.getValueByID(15012, XcomC.TYPE_FLOAT)
                solar_prod = xcom.getValueByID(15017, XcomC.TYPE_FLOAT)
                solar_prod1 = xcom.getValueByID(15018, XcomC.TYPE_FLOAT)
                solar_prod2 = xcom.getValueByID(15019, XcomC.TYPE_FLOAT)

                file.write(f'battCharge = {battCharge} Ah\n')
                file.write(f'batt_discharge = {batt_discharge} Ah\n')
                file.write(f'battpower = {battpower/1000} kW\n')
                file.write(f'soc = {soc} %\n')
                file.write(f'batttemp = {batttemp} °C\n')
                file.write(f'battery_voltage = {battery_voltage} V\n')
                file.write(f'grid_prod = {grid_prod} kWh\n')
                file.write(f'houseload = {houseload} kW\n')
                file.write(f'solarpower = {solarpower} kW\n')
                file.write(f'solarpower1 = {solarpower1} kW\n')
                file.write(f'solarpower2 = {solarpower2} kW\n')
                file.write(f'solar_prod = {solar_prod} kwh\n')
                file.write(f'solar_prod1 = {solar_prod1} kwh\n')
                file.write(f'solar_prod2 = {solar_prod2} kwh\n')

if __name__ == "__main__":
    main()

ha_xcom001.py script to call the library function to read Studer data and create and write to HA sensors. This script takes to arguments. The first argument is a count of the number of times that the loop will run. If this argument is set to a negative number like -1, it will run without stopping. The second argument is how many seconds to wait before repeating through the loop. 3 seconds seems to work well and gives the Studer portal time to upload Synoptic data, but not enough for the nightly datalogs. I am testing a 40 second delay starting 15 minutes before the portal clears the solar energy 11 PM for my system) data and running until 3 hours later. So far this seems to work.

import sys
import datetime
import uuid
import time
import requests
import logging

# Your XcomLANTCP and other custom imports go here
from xcom_proto import XcomP as param
from xcom_proto import XcomC
from xcom_proto import XcomLANTCP


class HomeAssistant:

    def __init__(self, url: str, token: str):
        self.url = url
        self.token = token
        self.log = logging.getLogger("HomeAssistant")

    def sendData(self, sensorName: str, readableName: str, sensorState, sensorUnit: str, binarySensor=False):
        if binarySensor:
            url = self.url + "binary_sensor." + sensorName
        else:
            url = self.url + "sensor." + sensorName

        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }

        if sensorUnit == "":
            attr = {
                "friendly_name": readableName
            }
        else:
            attr = {
                "unit_of_measurement": sensorUnit,
                "friendly_name": readableName
            }

        data = {
            "state": sensorState,
            "attributes": attr
        }

        self.log.debug(data)

        resp = requests.post(url=url, headers=headers, json=data)

        if 200 <= resp.status_code <= 201:
            self.log.debug(f"HA send OK ({resp.status_code})")
        else:
            self.log.error(f"HA send failed! ({resp.status_code} / {url})")
            

    def read_sensor(self, sensor_name: str):
        url = self.url + f"sensor.{sensor_name}"

        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }

        resp = requests.get(url=url, headers=headers)

        if 200 <= resp.status_code <= 201:
            data = resp.json()
            sensor_state = data.get("state")
            return sensor_state
        else:
            self.log.error(f"HA read failed! ({resp.status_code} / {url})")
            return None




HOME_ASS_URL = "http://<ip>:8123/api/states/"
HOME_ASS_TOKEN = "<your token>" # must be made using an administrative account, account should be local access only


ha = HomeAssistant(HOME_ASS_URL, HOME_ASS_TOKEN)

# Example read usage:
# ac_voltage_out_state = ha.read_sensor(sensor_name="scom_ac_voltage_out")
# print(f"The state of scom_ac_voltage_out is: {ac_voltage_out_state}")


def main():
    # Get the number of measurements and delay from command line arguments
    num_measurements = int(sys.argv[1]) if len(sys.argv) > 1 else 1
    delay = float(sys.argv[2]) if len(sys.argv) > 2 else 1
    
    # Initialize time since previous read variable for seconds since last read calculation and other loop variables
    previous_datetime = 0
    scom_error_code = 0
    previous_studer_python_script_loop_delay = 0
    previous_studer_smart_boost_limit = 0
    previous_studer_soc_level_for_backup = 0
    

    measurements = 0
    while (measurements < num_measurements) or (num_measurements < 0):
        try:
# Calculate seconds since last read
            start_datetime = datetime.datetime.now()
            if previous_datetime != 0:                
                delta = start_datetime - previous_datetime
                seconds_since_last_read = delta.total_seconds()                
                time.sleep(delay) # If this is not the first read, delay Xcom reads by delay seconds
            else:
                seconds_since_last_read = "NaN"
#            print("Seconds since last read =", seconds_since_last_read)  
            previous_datetime = start_datetime  
#            print("Count Down = ", num_measurements - measurements)
        
# Open Xcom channel and timestamp to calculate delays
            xcom_channel_open_start_datetime = datetime.datetime.now()
            with XcomLANTCP(port=4001) as xcom:				       # Move this to before the start of the while loop as in measurements17.  When it is in the loop, it delays the start of each block of reads by about 2 to 5 seconds.  Also include this in error handling routine because previous issue may have timed out after about 40 seconds.
                xcom_channel_open_complete_datetime = datetime.datetime.now()
                xcom_channel_open_time = (xcom_channel_open_complete_datetime - xcom_channel_open_start_datetime).total_seconds()
#                print("Xcom channel open time = ", xcom_channel_open_time)
                
# Perform Xcom reads        
    # Energy    
                has_solar = "NaN"                                                                     # 1 place holder, 11015 Model of VarioTrack
                solar_prod_vs1 = xcom.getValue(param.VS_PV_PROD, dstAddr=701)                         # 2A
                solar_prod_vs2 = xcom.getValue(param.VS_PV_PROD, dstAddr=702)                         # 2B
                yesterday_solar_energy_vs1 = xcom.getValue(param.VS_PV_ENERGY_PREV_DAY, dstAddr=701)  # 3A
                yesterday_solar_energy_vs2 = xcom.getValue(param.VS_PV_ENERGY_PREV_DAY, dstAddr=702)  # 3B
                hasInverter = "NaN"                                                                   # 4 place holder, 3124 ID type
                grid_prod = xcom.getValue(param.AC_ENERGY_IN_CURR_DAY)                                # 5
                ac_energy_in_prev_day = xcom.getValue(param.AC_ENERGY_IN_PREV_DAY)                    # 6
                ac_energy_out_curr_day = xcom.getValue(param.AC_ENERGY_OUT_CURR_DAY)                  # 7
                ac_energy_out_prev_day = xcom.getValue(param.AC_ENERGY_OUT_PREV_DAY)                  # 8
    # Battery                
                has_battery_status_processor = "NaN"                                                  # 9 place holder, 15074 ID type
                battery_charge = xcom.getValue(param.BATT_CHARGE)                                     # 10
                yesterday_battery_charge = xcom.getValue(param.BATT_CHARGE_PREV_DAY)                  # 11
                battery_discharge = xcom.getValue(param.BATT_DISCHARGE)                               # 12
                yesterday_battery_discharge = xcom.getValue(param.BATT_DISCHARGE_PREV_DAY)            # 13
                has_ac_coupling = "NaN"                                                               # 14 place holder
                ac_coupling = "NaN"                                                                   # 15 place holder
                yesterday_ac_coupling = "NaN"                                                         # 16 place holder
                battery_voltage = xcom.getValue(param.BATT_VOLTAGE)                                   # 17
                battery_current = xcom.getValue(param.BATT_CURRENT)                                   # 18
                soc = xcom.getValue(param.BATT_SOC)                                                   # 19
                battery_temp = xcom.getValue(param.BATT_TEMP)                                         # 20
                batt_cycle_phase = xcom.getValue(param.BATT_CYCLE_PHASE_XT)                           # 21
                soc_level_for_grid_feeding = xcom.getValue(param.SOC_LEVEL_FOR_GRID_FEEDING, dstAddr=601)
    # Power                
                solar_power_vs1 = xcom.getValue(param.VS_PV_POWER, dstAddr=701)                       # 22A
                solar_power_vs2 = xcom.getValue(param.VS_PV_POWER, dstAddr=702)                       # 22B
                gridpower = xcom.getValue(param.AC_POWER_IN)                                          # 23
                houseload = xcom.getValue(param.AC_POWER_OUT)                                         # 24
                battpower = xcom.getValue(param.BATT_POWER)                                           # 25
                has_ac_coupling = "NaN"                                                               # 26 place holder
                ac_coupling = "NaN"                                                                   # 27 place holder
                ac_in_current = xcom.getValue(param.AC_CURRENT_IN)                                    # 28
                ac_out_current = xcom.getValue(param.AC_CURRENT_OUT)                                  # 29
                ac_voltage_in = xcom.getValue(param.AC_VOLTAGE_IN)                                    # 30
                ac_voltage_out = xcom.getValue(param.AC_VOLTAGE_OUT)                                  # 31
                ac_freq_in = xcom.getValue(param.AC_FREQ_IN)                                          # 32
                ac_freq_out = xcom.getValue(param.AC_FREQ_OUT)                                        # 33

# Push data to Home Assistant        
    # Status                 

                ha.sendData("scom_error_code", "Scom Error Code", scom_error_code, " ")
                ha.sendData("scom_seconds_since_last_read", "Scom Seconds Since Last Read", seconds_since_last_read, "s")
    # Energy                 
                ha.sendData("scom_solar_prod_vs1", "Scom Solar Prod VS1", round(solar_prod_vs1, 2), "kWh")                                        #2A
                ha.sendData("scom_solar_prod_vs2", "Scom Solar Prod VS2", round(solar_prod_vs2, 2), "kWh")                                        #2B
                ha.sendData("scom_yesterday_solar_energy_vs1", "Scom Yesterday Solar Energy VS1", round(yesterday_solar_energy_vs1, 2), "kWh")    #3A
                ha.sendData("scom_yesterday_solar_energy_vs2", "Scom Yesterday Solar Energy VS2", round(yesterday_solar_energy_vs2, 2), "kWh")    #3B
                ha.sendData("scom_grid_prod", "Scom Grid Production", round(grid_prod, 2), "kWh")                                                 #5
                ha.sendData("scom_ac_energy_in_prev_day", "Scom AC Energy In Previous Day", round(ac_energy_in_prev_day, 2), "kWh")               #6
                ha.sendData("scom_ac_energy_out_curr_day", "Scom AC Energy Out Current Day", round(ac_energy_out_curr_day, 2), "kWh")             #7
                ha.sendData("scom_ac_energy_out_prev_day", "Scom AC Energy Out Previous Day", round(ac_energy_out_prev_day, 2), "kWh")            #8
    # Battery                
                ha.sendData("scom_battery_charge", "Scom Battery Charge", round(battery_charge, 2), "Ah")                                         #10
                ha.sendData("scom_yesterday_battery_charge", "Scom Yesterday Battery Charge", round(yesterday_battery_charge, 2), "Ah")           #11
                ha.sendData("scom_battery_discharge", "Scom Battery Discharge", round(battery_discharge, 2), "Ah")                                #12
                ha.sendData("scom_yesterday_battery_discharge", "Scom Yesterday Battery Discharge", round(yesterday_battery_discharge, 2), "Ah")  #13
                ha.sendData("scom_battery_voltage", "Scom Battery Voltage", round(battery_voltage, 2), "V")                                       #17
                ha.sendData("scom_battery_current", "Scom Battery Current", round(battery_current, 2), "A")                                       #18
                ha.sendData("scom_soc", "Scom Battery State of Charge", int(soc), "%")                                                            #19
                ha.sendData("scom_battery_temp", "Scom Battery Temperature", round(battery_temp, 2), "°C")                                        #20
                ha.sendData("scom_batt_cycle_phase", "Scom Battery Cycle Phase", batt_cycle_phase, "")                                            #21
                ha.sendData("scom_soc_level_for_grid_feeding", "Scom SOC Level for Grid Feeding", soc_level_for_grid_feeding, "%")   
   # Power                
                ha.sendData("scom_solar_power_vs1", "Scom Solar Power VS1", round(solar_power_vs1, 3), "kW")                                      #22A
                ha.sendData("scom_solar_power_vs2", "Scom Solar Power VS2", round(solar_power_vs2, 3), "kW")                                      #22B                
                ha.sendData("scom_gridpower", "Scom Grid Power", round(gridpower, 3), "kW")                                                       #23
                ha.sendData("scom_houseload", "Scom Houseload Power", round(houseload, 3), "kW")                                                  #24
                ha.sendData("scom_battpower", "Scom Battery Power", round(battpower/1000, 3), "kW")                                               #25
                ha.sendData("scom_ac_in_current", "Scom AC In Current", round(ac_in_current, 2), "A")                                             #28
                ha.sendData("scom_ac_out_current", "Scom AC Out Current", round(ac_out_current, 2), "A")                                          #29
                ha.sendData("scom_ac_voltage_in", "Scom AC Voltage In", int(ac_voltage_in), "V")                                                  #30
                ha.sendData("scom_ac_voltage_out", "Scom AC Voltage Out", int(ac_voltage_out), "V")                                               #31
                ha.sendData("scom_ac_freq_in", "Scom AC Frequency In", round(ac_freq_in, 2), "Hz")                                                #32
                ha.sendData("scom_ac_freq_out", "Scom AC Frequency Out", round(ac_freq_out, 2), "Hz")                                             #33
                                       
   # Read values from HA, test if changed, and if changed, print to console, write to Studer, or use to control delay time in this script                                 
#                current_studer_python_script_loop_delay = ha.read_sensor(sensor_name="ha_scom_studer_python_script_loop_delay")
#                if current_studer_python_script_loop_delay != previous_studer_python_script_loop_delay:
#                    delay = float(current_studer_python_script_loop_delay)
#                    print(f"At {start_datetime} the Current Studer Python Script Loop Delay is: {current_studer_python_script_loop_delay} s")
#                    previous_studer_python_script_loop_delay = current_studer_python_script_loop_delay
                    
#                current_studer_smart_boost_limit = ha.read_sensor(sensor_name="ha_scom_studer_smart_boost_limit")
#                if current_studer_smart_boost_limit != previous_studer_smart_boost_limit:
#                    xcom.setValue(param.SMART_BOOST_LIMIT, float(current_studer_smart_boost_limit))
#                    print(f"At {start_datetime} the Current Studer Smart Boost Limit is: {current_studer_smart_boost_limit} %")
#                    previous_studer_smart_boost_limit = current_studer_smart_boost_limit       
                    
#                current_studer_soc_level_for_backup = ha.read_sensor(sensor_name="ha_scom_studer_soc_level_for_backup")
#                if current_studer_soc_level_for_backup != previous_studer_soc_level_for_backup:
#                    if float(current_studer_soc_level_for_backup) >= float(soc_level_for_grid_feeding):  # The maximum allowed value is equal to SOC Level for Grid Feeding
#                        current_studer_soc_level_for_backup = soc_level_for_grid_feeding - 1.0   
#                    xcom.setValue(param.SOC_LEVEL_FOR_BACKUP, float(current_studer_soc_level_for_backup), dstAddr=601)
#                    print(f"At {start_datetime} the Current Studer SOC Level for Backup is: {current_studer_soc_level_for_backup} %")
#                    previous_studer_soc_level_for_backup = current_studer_soc_level_for_backup                        
                
                scom_error_code = 0
                if num_measurements >= 0:
                    measurements += 1
                
        except AssertionError:
            print("Assertion Error at ", datetime.datetime.now())    
            scom_error_code = 1

if __name__ == "__main__":
    main()

Example helpers to control input templates:

Templates to receive values from input templates for python script to read and send to Studer system:


- name: "HA SCOM Studer Python Script Loop Delay"
  unique_id: "ha_studer_python_script_loop_delay"
  unit_of_measurement: "s"
  device_class: "duration"
  state_class: "measurement"
  state: >-
    {% set delay_time = states('input_number.studer_python_script_loop_delay' )|float() %}
    {{ delay_time }}

- name: "HA SCOM Studer Smart Boost Limit"
  unique_id: "ha_studer_smart_boost_limit"
  unit_of_measurement: "%"
  device_class: "battery"
  state_class: "measurement"
  state: >-
    {% set smart_boost = states('input_number.studer_smart_boost_limit' )|float() %}
    {{ smart_boost }}

- name: "HA SCOM Studer SOC Level for Backup"
  unique_id: "ha_studer_soc_level_for_backup"
  unit_of_measurement: "%"
  device_class: "battery"
  state_class: "measurement"
  state: >-
    {% set soc_level_for_backup = states('input_number.studer_soc_level_for_backup' )|float() %}
    {% set soc_level_for_grid_feeding = states('sensor.scom_soc_level_for_grid_feeding' )|float() %}
    {% if soc_level_for_backup > soc_level_for_grid_feeding %}
      {{ soc_level_for_grid_feeding }}      
    {% else %}
      {{ soc_level_for_backup }}
    {% endif %}

Once the three sensors:
sensor.ha_studer_python_script_loop_delay
sensor.ha_studer_smart_boost_limit
sensor.ha_studer_soc_level_for_backup
in the last two steps have been created and respond to the numerical input helpers, you can uncomment the lines near the end of the script that read these sensors and rerun the script. The Studer should respond to changes in the input helper values.

Many thanks for sharing this! Very interresting and good information. I had just tested your Studer cloud RESTful integration examples found at your another post. I got the samples working right away, no issues.
I shall certainly try this when I have time.

1 Like

Glad to hear that you got the previous cloud approach working quickly. Please let me know if you make any improvements in this local one. Also give thanks to Zocker, the owner of the Xcom-LAN library functions. He is the one that did the heavy lifting.

1 Like

Thank you so much for sharing your work on that.
I tried your step by step instructions but get stuck on the read test with that :


Can you maybe help?
thanks

If you exit python, then run “pip list” at the command prompt, does xcom_proto 0.3.3 appear on the list of installed packages?

after the

houseload = xcom.getValue(param.AC_POWER_OUT)

try hitting the enter key and wait for the “>>>” python prompt to appear before you enter the print commands.

Also, check that your Moxa is configured like this where you have added the ip address of the machine that is running the python script on the destination ip address line 2. Also the data packing section of the Moxa configuration should be the same as shown.

Thank you for your answer. I tried with Python prompt line by line but still the same problem. Probably a Python problem. I will reinstall xcom lan library.
my Moxa config is as shown in your answer.

Had you also run these three import commands prior to all other python commands?

Yes I did.
I tried to reinstall xcom-protocol-master.zip. I still have same error.

By the way thank you very much for your Studer RESTFUL tutorial, It is working very well for me : ( https://community.home-assistant.io/t/studer-innotec-solar-components-integration )

1 Like

I just tried it and found that from the command prompt, I have to execute the assign statements separate from the print statements. Try entering:

with XcomLANTCP(port=4001) as xcom:
    soc = xcom.getValue(param.BATT_SOC)  
    gridpower = xcom.getValue(param.AC_POWER_IN) 
    houseload = xcom.getValue(param.AC_POWER_OUT)
  

Then hit the return key to get back the python command prompt “>>>”

Then enter the print statements:

print('soc = ',soc, '%')	     
print('gridpower = ',gridpower, 'kW')
print('houseload = ',houseload, 'kW')

Other things to check:

It seems like the assignment statements aren’t assigning values to the variables soc, gridpower and houseload. Are you sure that you entered the correct ip address of the machine where the python package was installed into the Moxa ip server list? Could you have a firewall or anything else on that machine that blocks ip traffic?

To install the Xcom library and run the script locally on the same pi that runs HA, I followed the instructions here for creating a venv virtual python environment: Installing Red on Debian 12 Bookworm — Red - Discord Bot 3.5.5 documentation

Later I will look into making the script self starting so that it will auto start whenever the pi restarts or if the script were to stop.

It is now working after having tried your last recommandation to go back to comamnd prompt, thanks ! : ) :+1:

1 Like