weltenwort / home-assistant-rct-power-integration

A Home Assistant custom component to integrate with RCT Power inverters.
MIT License
67 stars 15 forks source link

Control the system (actually set battery charge level dynamically) #376

Open heikone opened 6 months ago

heikone commented 6 months ago

Hello together,

just a thing, that came to my mind: Is there a possibility to change the charge level of the battery beside setting it in the RCT app? My goal would be to control the charge level dynamically from Homeassistant.

I believe this is an enhancement and maybe somebody give any hint how to manage this.

Greetings, Heiko

rhoegen commented 6 months ago

I would also appreciate this feature, since the internal SoC feature of the RCT devices is not as flexible as I would want it to be. It would be sufficient to change the "non-island" Target SoC, which is available in the app through the installer menu.

Happy to serve as a beta-tester for the feature... I have 2 PowerStorage DC 10.0 in Master/Slave Mode

hanzsez commented 6 months ago

+1 to this, even it would mean that the devs have to implement the write function (which they do not want to, for obvious reasons). But having just battery soc manipulation as write would benefit me a lot as well

philoxio commented 5 months ago

This would be a huge benefit. Specially if you want to charge an EV and "hold" the SoC of the battery, instead of having to wait for the battery to be fully charged.

heikone commented 4 months ago

@philoxio, this exactly my case of application!

kaitimmer commented 4 months ago

I agree, being able to control the SoC Target and maybe max SoC would be awesome.

do-gooder commented 1 month ago
#!/usr/bin/env python3

## power_mng.soc_strategy       0: SOC-Ziel = SOC, 1: Konstant, 2: Extern, 3: Mittlere Batteriespannung, 4: Intern, 5: Zeitplan (default: 4)
## power_mng.soc_target_set     Force SOC target (default: 0.5) --> (1: Konstant)
## power_mng.battery_power_extern   Battery target power (positive = discharge) --> (2. Extern)

## power_mng.soc_min            Min SOC target (default 0.07) --> ggf. als Ersatzstrom-Reserve
## power_mng.soc_charge_power       Charging to power_mng.soc_min with given W (default: 100)
## power_mng.soc_charge         Trigger for charing to soc_min (default: 0.05)

import sys
import socket
import select
from rctclient.frame import make_frame, ReceiveFrame
from rctclient.registry import REGISTRY
from rctclient.types import Command
from rctclient.utils import decode_value, encode_value

def show_help():
    print("Usage:")
    print("  rct.py get <parameter> --host=<ip_address_or_hostname>")
    print("  rct.py set <parameter> <value> --host=<ip_address_or_hostname>")
    print("\nValid Parameters:")
    print("  power_mng.soc_strategy - SOC charging strategy")
    print("    Valid Values:")
    print("      0: SOC target = SOC (State of Charge)")
    print("      1: Konstant (Constant)")
    print("      2: Extern (External)")
    print("      3: Mittlere Batteriespannung (Average Battery Voltage)")
    print("      4: Intern (Internal)")
    print("      5: Zeitplan (Schedule)")
    print("    Default Value: 4 (Internal)")
    print("  power_mng.soc_target_set - Force SOC target")
    print("    Valid Range: 0.00 to 1.00, with at most two decimal places")
    print("    Default Value: 0.50")
    print("  power_mng.battery_power_extern - Battery target power")
    print("    Valid Range: -6000 to 6000")
    print("      Positive values indicate discharge, negative values indicate charge")
    print("    Default Value: 0")
    print("  power_mng.soc_min - Min SOC target")
    print("    Valid Range: 0.00 to 1.00, with at most two decimal places")
    print("    Default Value: 0.07")
    print("  power_mng.soc_charge_power - Charging power to reach SOC target")
    print("    Default Value: 100")
    print("  power_mng.soc_charge - Trigger for charging to SOC_min")
    print("    Default Value: 0.05")

def set_value(parameter, value, host):
    valid_parameters = [
        "power_mng.soc_strategy",
        "power_mng.soc_target_set",
        "power_mng.battery_power_extern",
        "power_mng.soc_min",
        "power_mng.soc_charge_power",
        "power_mng.soc_charge"
    ]

    if parameter not in valid_parameters:
        print(f"Error: Invalid parameter '{parameter}'.")
        show_help()
        sys.exit(1)

    if parameter == "power_mng.soc_strategy" and value not in ["0", "1", "2", "3", "4", "5"]:
        print(f"Error: Invalid value '{value}' for parameter '{parameter}'.")
        show_help()
        sys.exit(1)
    elif parameter == "power_mng.soc_strategy":
        try:
            value = int(value)
        except ValueError:
            print(f"Error: Invalid value '{value}' for parameter '{parameter}'.")
            show_help()
            sys.exit(1)
    elif parameter == "power_mng.soc_target_set":
        try:
            value = float(value)
            if not (0.00 <= value <= 1.00) or len(str(value).split(".")[1]) > 2:
                raise ValueError
        except ValueError:
            print(f"Error: Invalid value '{value}' for parameter '{parameter}'.")
            show_help()
            sys.exit(1)
    elif parameter == "power_mng.battery_power_extern":
        try:
            value = float(value)
            if not (-6000 <= value <= 6000):
                raise ValueError
        except ValueError:
            print(f"Error: Invalid value '{value}' for parameter '{parameter}'.")
            show_help()
            sys.exit(1)
    elif parameter == "power_mng.soc_min":
        try:
            value = float(value)
            if not (0.00 <= value <= 1.00) or len(str(value).split(".")[1]) > 2:
                raise ValueError
        except ValueError:
            print(f"Error: Invalid value '{value}' for parameter '{parameter}'.")
            show_help()
            sys.exit(1)
    elif parameter in ["power_mng.soc_charge_power", "power_mng.soc_charge"]:
        try:
            value = float(value)
        except ValueError:
            print(f"Error: Invalid value '{value}' for parameter '{parameter}'.")
            show_help()
            sys.exit(1)

    object_name = parameter
    command = Command.WRITE
    host_port = (host, 8899)

    object_info = REGISTRY.get_by_name(object_name)
    encoded_value = encode_value(data_type=object_info.request_data_type, value=value)
    send_frame = make_frame(command=command, id=object_info.object_id, payload=encoded_value)

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(host_port)
    sock.send(send_frame)
    sock.close()

    print(f"Setting value {value} for parameter {parameter} on host {host}")

def get_value(parameter, host):
    valid_parameters = [
        "power_mng.soc_strategy",
        "power_mng.soc_target_set",
        "power_mng.battery_power_extern",
        "power_mng.soc_min",
        "power_mng.soc_charge_power",
        "power_mng.soc_charge"
    ]

    if parameter not in valid_parameters:
        print(f"Error: Invalid parameter '{parameter}'.")
        show_help()
        sys.exit(1)

    object_name = parameter
    command = Command.READ
    host_port = (host, 8899)

    object_info = REGISTRY.get_by_name(object_name)
    send_frame = make_frame(command=command, id=object_info.object_id)

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(host_port)
    sock.send(send_frame)

    # Receive the response
    response_frame = ReceiveFrame()
    while True:
        ready_read, _, _ = select.select([sock], [], [], 2.0)
        if sock in ready_read:
            # receive content of the input buffer
            buf = sock.recv(256)
            # if there is content, let the frame consume it
            if len(buf) > 0:
                response_frame.consume(buf)
                # if the frame is complete, we're done
                if response_frame.complete():
                    break
            else:
                # the socket was closed by the device, exit
                sys.exit(1)

    # Decode the response value
    decoded_value = decode_value(object_info.response_data_type, response_frame.data)

    sock.close()

    #print(f"The decoded value for '{parameter}' on host {host} is: {decoded_value}")
    print(f"{decoded_value}")

# Main script logic
if __name__ == "__main__":
    if len(sys.argv) < 4 or sys.argv[1] in ("-h", "--help"):
        show_help()
        sys.exit(1)

    subcommand = sys.argv[1]
    host = None

    # Durchlaufe die Argumente, um die Host-Adresse zu extrahieren
    for arg in sys.argv[2:]:
        if arg.startswith("--host="):
            host = arg[len("--host="):]
            break

    if not host:
        print("Error: Host parameter is missing.")
        show_help()
        sys.exit(1)

    if subcommand == "set":
        if len(sys.argv) != 5:
            print("Error: Please provide a parameter, a value, and a host to set.")
            show_help()
            sys.exit(1)
        set_value(sys.argv[2], sys.argv[3], host)
    elif subcommand == "get":
        if len(sys.argv) != 4:
            print("Error: Please provide a parameter and a host to get.")
            show_help()
            sys.exit(1)
        get_value(sys.argv[2], host)
    else:
        print(f"Error: Unknown command '{subcommand}'.")
        show_help()
        sys.exit(1)
heikone commented 1 month ago

Thanks a lot for this proposal! I've tried your script to GET values from my system and it works.

But I'm confused a little bit, because I've limited maximum SOC to 80% (with strategy "constant") at the moment and I guess, the proper method is power_mng.soc_target_set to get this. But I was wrong. python test.py get power_mng.soc_target_set --host=192.168.0.99 is reporting 1.0 instead of expected 0.8.

My general goal is to set maximum SOC limit manually by input number from Homeassistant. This should later enable to use solar surplus to charge car before charging RCT's battery. but step, by step, ...

Maybe you advice some help!

Greetings, Heiko

heikone commented 1 month ago

Hmm, strange. Meanwhile, I've SET power_mng.soc_strategyto 1 and power_mng.soc_target_setto 0.82. The GET command reports, the values are set, but the inverter does not act according. While sun is shining and battery is at 80%, I've expected, that the system will now charge to 82%. But nothing happens!

Any advice / hint for me? Is maybe flashing or any other command by rctclient needed additionally, @do-gooder?

do-gooder commented 1 month ago

Unfortunately there is no documentation. We have to find out for ourselves how the settings work.

Special RCT Infos

Starting on Page 32

@heikone You don't need to flash! Please share your insights!

heikone commented 1 month ago

The first one is helpful, I think. @do-gooder, did you test to use power_mng.soc_max instead of power_mng.soc_target_set to dynamically limit the charge? This register is listed in rctclient documentation and also #8 and #12 in thread "RCT Power Storage - SOC Zielauswahl "Extern" nutzen" you provided. But I agree to you - documentation is a mess here.

do-gooder commented 1 month ago

My script is more than 6 months old. But I don't use it due to lack of time.

@heikone Please use heiphoss instand of installer for the android app! But be careful! Here you can test your settings better!

do-gooder commented 1 month ago

Once we have figured out how this all works, we could write a wrapper that provides the functions such as charging from the grid for Tibber, changing the SOC, etc. via an HTTP API. This is what RCT Power should have done.

heikone commented 1 month ago

Hi, meanwhile I've tested to change 'power_mng.soc_max' by adding this to your script in the same way 'power_mng.soc_min' was already available. Result: Works as expected! Inverter is is immediately accepting new maximum SOC level and further charges or stops, depending on the actual SOC.

I agree, this could be the basis for any other solution. Just changing the maximum SOC seems to be of limited and acceptable risk for me at the moment. We should remember, that writing settings is not recommended by the rctclient documentation ...

When I can find time during my vacation, I will propose a solution for adjusting maximum SOC from input number of Homeassistant.

Greetings, Heiko

do-gooder commented 1 month ago

Ok, please use rctpower_writesupport for your updates!