Skip to content

Commit

Permalink
Sunsynk controls
Browse files Browse the repository at this point in the history
  • Loading branch information
fboundy committed Apr 4, 2024
1 parent 261f60d commit 2992f43
Showing 1 changed file with 181 additions and 78 deletions.
259 changes: 181 additions & 78 deletions apps/pv_opt/sunsynk.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import pandas as pd
import time
import json

TIMEFORMAT = "%H:%M"
INVERTER_DEFS = {
"SUNSYNK_SOLARSYNK2": {
"online": "sensor.{device_name}_{inverter_sn}_battery_soc",
# "modes": {
# 1: "Selfuse - No Grid Charging",
# 3: "Timed Charge/Discharge - No Grid Charging",
Expand All @@ -30,18 +32,52 @@
# file. They are required for the main PV_Opt module and if they cannot be found an ERROR will be
# raised
"default_config": {
"maximum_dod_percent": "sensor.{device_name}_{inverter sn}_battery_shutdown_cap",
"id_battery_soc": " sensor.{device_name}_{inverter sn}_battery_soc",
"id_consumption_today": "sensor.{device_name}_{inverter sn}_day_load_energy",
"id_grid_import_today": "sensor.{device_name}_{inverter sn}_day_grid_import",
"id_grid_export_today": "sensor.{device_name}_{inverter sn}_day_grid_import",
"maximum_dod_percent": 20,
"id_battery_soc": "sensor.{device_name}_{inverter_sn}_battery_soc",
"id_consumption_today": "sensor.{device_name}_{inverter_sn}_day_load_energy",
"id_grid_import_today": "sensor.{device_name}_{inverter_sn}_day_grid_import",
"id_grid_export_today": "sensor.{device_name}_{inverter_sn}_day_grid_export",
"supports_hold_soc": False,
"update_cycle_seconds": 300,
},
# Brand Conguration: Exposed as inverter.brand_config and can be over-written using arguments
# from the config.yaml file but not rquired outside of this module
# from the config.yaml file but not required outside of this module
"brand_config": {

"battery_voltage": "sensor.{device_name}_{inverter_sn}_battery_voltage",
"battery_current": "sensor.{device_name}_{inverter_sn}_battery_current",
"id_control_helper": "input_text.{device_name}_{inverter_sn}_settings",
"id_use_timer": "sensor.{device_name}_{inverter_sn}_use_timer",
"id_priority_load": "sensor.{device_name}_{inverter_sn}_priority_load",
"id_timed_charge_start": "sensor.{device_name}_{inverter_sn}_prog1_time",
"id_timed_charge_end": "sensor.{device_name}_{inverter_sn}_prog2_time",
"id_timed_charge_unused": ["sensor.{device_name}_{inverter_sn}_" + f"prog{i}_time" for i in range(2, 7)],
"id_timed_charge_enable": "sensor.{device_name}_{inverter_sn}_prog1_charge",
"id_timed_charge_capacity": "sensor.{device_name}_{inverter_sn}_prog1_capacity",
"id_timed_discharge_start": "sensor.{device_name}_{inverter_sn}_prog3_time",
"id_timed_discharge_end": "sensor.{device_name}_{inverter_sn}_prog4_time",
"id_timed_discharge_unused": [
"sensor.{device_name}_{inverter_sn}_" + f"prog{i}_time" for i in [1, 2, 5, 6]
],
"id_timed_dicharge_enable": "sensor.{device_name}_{inverter_sn}_prog3_charge",
"id_timed_discharge_capacity": "sensor.{device_name}_{inverter_sn}_prog3_capacity",
"json_work_mode": "sysWorkMode",
"json_priority_load": "energyMode",
"json_grid_charge": "sdChargeOn",
"json_use_timer": "peakAndVallery",
"json_timed_charge_start": "sellTime1",
"json_timed_charge_end": "sellTime2",
"json_timed_charge_unused": [f"sellTime{i}" for i in range(2, 7)],
"json_timed_charge_enable": "time1on",
"json_timed_charge_target_soc": "cap1",
"json_charge_current": "sdBatteryCurrent",
"json_gen_charge_enable": "genTime1on",
"json_timed_discharge_start": "sellTime3",
"json_timed_discharge_end": "sellTime4",
"json_timed_discharge_unused": [f"sellTime{i}" for i in [1, 2, 5, 6]],
"json_timed_discharge_enable": "time3on",
"json_timed_discharge_target_soc": "cap3",
"json_timed_discharge_power": "sellTime3Pac",
"json_gen_discharge_enable": "genTime3on",
},
},
}
Expand All @@ -53,6 +89,8 @@ def __init__(self, inverter_type, host) -> None:
self.tz = self.host.tz
if host is not None:
self.log = host.log
self.tz = self.host.tz

self.type = inverter_type
self.config = {}
self.brand_config = {}
Expand All @@ -62,97 +100,162 @@ def __init__(self, inverter_type, host) -> None:
):
for item in defs:
if isinstance(defs[item], str):
conf[item] = defs[item].replace(
"{device_name}", self.host.device_name
)
conf[item] = defs[item].replace(
"{inverter_sn}", self.host.inverter_sn
)
conf[item] = defs[item].replace("{device_name}", self.host.device_name)
conf[item] = defs[item].replace("{inverter_sn}", self.host.inverter_sn)
elif isinstance(defs[item], list):
conf[item] = [
z.replace("{device_name}", self.host.device_name)
for z in defs[item]
]
conf[item] = [
z.replace("{inverter_sn}", self.host.inverter_sn)
for z in defs[item]
]
conf[item] = [z.replace("{device_name}", self.host.device_name) for z in defs[item]]
conf[item] = [z.replace("{inverter_sn}", self.host.inverter_sn) for z in defs[item]]
else:
conf[item] = defs[item]

def is_online(self):
entity_id = INVERTER_DEFS[self.type].get("online", (None, None))
if entity_id is not None:
entity_id = entity_id.replace("{device_name}", self.host.device_name)
return self.host.get_state(entity_id) not in ["unknown", "unavailable"]
else:
return True

def _unknown_inverter(self):
e = f"Unknown inverter type {self.type}"
self.log(e, level="ERROR")
self.host.status(e)
raise Exception(e)

def _solarsynk_set_helper(self, **kwargs):
current_json = json.loads(self.host.get_config("id_control_helper"))
new_json = json.dumps(current_json | kwargs)
entity_id = self.host.config("id_control_helper")
self.rlog("Setting SolarSync input helper {entity_id} to {new_json}")
# self.host.set_state(entity_id=entity_id, state=new_json)

def enable_timed_mode(self):
if (
self.type == "SUNSYNK_SOLARSYNC2"
):
pass
if self.type == "SUNSYNK_SOLARSYNK2":
params = {
self.config["json_use_timer"]: 1,
self.config["json_priority_load"]: 1,
}
self._solarsynk_set_helper(params)

else:
self._unknown_inverter()

def control_charge(self, enable, **kwargs):
if enable:
self.enable_timed_mode()
self._control_charge_discharge("charge", enable, **kwargs)
if self.type == "SUNSYNK_SOLARSYNK2":
time_now = pd.Timestamp.now(tz=self.tz)

if enable:
self.enable_timed_mode()
params = {
self.config["json_work_mode"]: 2,
self.config["json_timed_charge_target_soc"]: kwargs.get("target_soc", 100),
self.config["json_timed_charge_start"]: kwargs.get("start", time_now.strftime(TIMEFORMAT)),
self.config["json_timed_charge_end"]: kwargs.get(
"end", time_now.ceil("30min").strftime(TIMEFORMAT)
),
self.config["json_charge_current"]: kwargs.get("power", 0)
/ self.host.get_config("battery_voltage"),
self.config["json_timed_charge_enable"]: True,
self.config["json_gen_charge_enable"]: False,
} | {x: "00:00" for x in self.config["json_timed_charge_unused"]}

self._solarsynk_set_helper(params)

else:
params = {
self.config["json_work_mode"]: 2,
self.config["json_target_soc"]: 100,
self.config["json_timed_charge_start"]: "00:00",
self.config["json_timed_charge_end"]: "00:00",
self.config["json_charge_current"]: self.host.charger_power,
self.config["json_timed_charge_enable"]: False,
self.config["json_gen_charge_enable"]: True,
} | {x: "00:00" for x in self.config["json_timed_charge_unused"]}
else:
self._unknown_inverter()

def control_discharge(self, enable, **kwargs):
if enable:
self.enable_timed_mode()
self._control_charge_discharge("discharge", enable, **kwargs)
if self.type == "SUNSYNK_SOLARSYNK2":
time_now = pd.Timestamp.now(tz=self.tz)

def hold_soc(self, enable, soc=None):
if (
self.type == "SUNSYNK_SOLARSYNC2"
):
if enable:
self._solis_set_mode_switch(
SelfUse=True, Timed=False, GridCharge=True, Backup=True
)
else:
self.enable_timed_mode()
params = {
self.config["json_work_mode"]: 0,
self.config["json_timed_discharge_target_soc"]: kwargs.get(
"target_soc", self.host.get_config("maximum_dod_percent")
),
self.config["json_timed_discharge_start"]: kwargs.get("start", time_now.strftime(TIMEFORMAT)),
self.config["json_timed_discharge_end"]: kwargs.get(
"end", time_now.ceil("30min").strftime(TIMEFORMAT)
),
self.config["json_discharge_power"]: kwargs.get("power", 0),
self.config["json_timed_discharge_enable"]: True,
self.config["json_gen_discharge_enable"]: False,
} | {x: "00:00" for x in self.config["json_timed_discharge_unused"]}

self._solarsynk_set_helper(params)

# Waiyt for a second to make sure the mode is correct
time.sleep(1)

if soc is None:
soc = self.host.get_config("maximum_dod_percent")

entity_id = self.host.config["id_backup_mode_soc"]

self.log(f"Setting Backup SOC to {soc}%")
if self.type == "SOLIS_SOLAX_MODBUS":
changed, written = self._write_and_poll_value(
entity_id=entity_id, value=soc
)
elif self.type == "SOLIS_CORE_MODBUS" or self.type == "SOLIS_SOLARMAN":
changed, written = self.solis_write_holding_register(
address=INVERTER_DEFS(self.type)["registers"]["backup_mode_soc"],
value=soc,
entity_id=entity_id,
)
else:
e = "Unknown inverter type"
self.log(e, level="ERROR")
raise Exception(e)
params = {
self.config["json_work_mode"]: 2,
self.config["json_timed_discharge_target_soc"]: 100,
self.config["json_timed_discharge_start"]: "00:00",
self.config["json_timed_discharge_end"]: "00:00",
self.config["json_discharge_power"]: 0,
self.config["json_timed_discharge_enable"]: False,
self.config["json_gen_discharge_enable"]: True,
} | {x: "00:00" for x in self.config["json_timed_discharge_unused"]}
else:
self._unknown_inverter()

def hold_soc(self, enable, soc=None):
if self.type == "SUNSYNK_SOLARSYNK2":
pass

else:
self._unknown_inverter()

@property
def status(self):
status = None
if (
self.type == "SUNSYNK_SOLARSYNC2"
):
status = "Status"
time_now = pd.Timestamp.now(tz=self.tz)

return status
if self.type == "SUNSYNK_SOLARSYNK2":
charge_start = pd.Timestamp(self.host.get_config("id_timed_charge_start"), tz=self.tz)
charge_end = pd.Timestamp(self.host.get_config("id_timed_charge_end"), tz=self.tz)
discharge_start = pd.Timestamp(self.host.get_config("id_timed_charge_start"), tz=self.tz)
discharge_end = pd.Timestamp(self.host.get_config("id_timed_charge_end"), tz=self.tz)

def _write_and_poll_value(self, entity_id, value, tolerance=0.0, verbose=False):
changed = False
written = False
return (changed, written)
status = {
"timer mode": self.host.get_config("id_use_timer"),
"priority load": self.host.get_config("id_priority_load"),
"charge": {
"start": charge_start,
"end": charge_end,
"active": self.host.get_config("id_timed_charge_enable")
and (time_now >= charge_start)
and (time_now < charge_end),
"target_soc": self.host.get_config("id_timed_charge_target_soc"),
},
"discharge": {
"start": discharge_start,
"end": discharge_end,
"active": self.host.get_config("id_timed_discharge_enable")
and (time_now >= discharge_start)
and (time_now < discharge_end),
"target_soc": self.host.get_config("id_timed_discharge_target_soc"),
},
"hold_soc": {
"active": False,
"soc": 0.0,
},
}

def _monitor_target_soc(self, target_soc, mode="charge"):
pass

def _control_charge_discharge(self, direction, enable, **kwargs):
if (
self.type == "SOLIS_SOLAX_MODBUS"
):
pass
return status

else:
self._unknown_inverter()

def _monitor_target_soc(self, target_soc, mode="charge"):
pass

0 comments on commit 2992f43

Please sign in to comment.