Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
heikone opened this issue Mar 10, 2024 · 14 comments
Open

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

heikone opened this issue Mar 10, 2024 · 14 comments

Comments

@heikone
Copy link

heikone commented Mar 10, 2024

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
Copy link

rhoegen commented Mar 14, 2024

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
Copy link

hanzsez commented Mar 15, 2024

+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
Copy link

philoxio commented Apr 7, 2024

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
Copy link
Author

heikone commented Apr 23, 2024

@philoxio, this exactly my case of application!

@kaitimmer
Copy link

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

@do-gooder
Copy link

do-gooder commented Aug 2, 2024

#!/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
Copy link
Author

heikone commented Aug 2, 2024

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
Copy link
Author

heikone commented Aug 2, 2024

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
Copy link

do-gooder commented Aug 2, 2024

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
Copy link
Author

heikone commented Aug 2, 2024

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
Copy link

do-gooder commented Aug 2, 2024

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
Copy link

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
Copy link
Author

heikone commented Aug 3, 2024

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
Copy link

do-gooder commented Aug 3, 2024

Ok, please use rctpower_writesupport for your updates!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants