Skip to content

Commit

Permalink
Solax fixed + Test function
Browse files Browse the repository at this point in the history
  • Loading branch information
fboundy committed Apr 4, 2024
1 parent 6ecd881 commit 06e6528
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 23 deletions.
123 changes: 105 additions & 18 deletions apps/pv_opt/pv_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
"number": {
"mode": "slider",
},
"text": {
"pattern": "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]",
},
}

DOMAIN_ATTRIBUTES = {
Expand All @@ -106,6 +109,47 @@
},
"domain": "number",
},
"test_start": {"default": "00:00", "domain": "text", "min": 5, "max": 5},
"test_end": {"default": "00:00", "domain": "text", "min": 5, "max": 5},
"test_power": {
"default": 3000,
"domain": "number",
"attributes": {
"min": 1000,
"max": 10000,
"step": 100,
"unit_of_measurement": "W",
"device_class": "power",
"mode": "slider",
},
},
"test_target_soc": {
"default": 100,
"domain": "number",
"attributes": {
"min": 0,
"max": 100,
"step": 1,
"unit_of_measurement": "%",
"device_class": "battery",
"mode": "slider",
},
},
"test_enable": {
"default": "Enable",
"domain": "select",
"attributes": {"options": ["Enable", "Disable"]},
},
"test_function": {
"default": "Charge",
"domain": "select",
"attributes": {"options": ["Charge", "Discharge"]},
},
"test_button": {
"default": pd.Timestamp.now(tz="UTC"),
"name": "Test",
"domain": "button",
},
"solcast_confidence_level": {
"default": 50,
"attributes": {
Expand Down Expand Up @@ -448,6 +492,42 @@ def initialize(self):
for id in self.handles:
self.log(f" {id} {self.handles[id]} {self.info_listen_state(self.handles[id])}")

@ad.app_lock
def _run_test(self):
self.ulog("Test")

test = {
item: self.get_ha_value(self.ha_entities[f"test_{item}"])
for item in ["start", "end", "power", "enable", "function", "target_soc"]
}

for x in ["start", "end"]:
test[x] = pd.Timestamp(test[x], tz=self.tz)

test["enable"] = test["enable"].lower() == "enable"
function = test.pop("function").lower()

self._log_inverter_status(self.inverter.status)

if function == "charge":
self.inverter.control_charge(**test)

elif function == "discharge":
self.inverter.control_discharge(**test)

else:
pass

if self.get_config("update_cycle_seconds") is not None:
i = int(self.get_config("update_cycle_seconds") * 1.2)
self.log(f"Waiting for Modbus Read cycle: {i} seconds")
while i > 0:
self._status(f"Waiting for Modbus Read cycle: {i}")
time.sleep(1)
i -= 1

self._log_inverter_status(self.inverter.status)

def _check_for_io(self):
self.ulog("Checking for Intelligent Octopus")
entity_id = f"binary_sensor.octopus_energy_{self.get_config('octopus_account').lower().replace('-', '_')}_intelligent_dispatching"
Expand Down Expand Up @@ -671,7 +751,7 @@ def _setup_schedule(self):
interval=self.get_config("optimise_frequency_minutes") * 60,
)
self.log(
f"Optimiser will run every {self.get_config('optimise_frequency_minutes')} minutes from {start_opt.strftime('%H:%M')} or on {EVENT_TRIGGER} Event"
f"Optimiser will run every {self.get_config('optimise_frequency_minutes')} minutes from {start_opt.strftime('%H:%M %Z')} or on {EVENT_TRIGGER} Event"
)

def _load_contract(self):
Expand Down Expand Up @@ -1276,9 +1356,12 @@ def _expose_configs(self, over_write=True):

self.mqtt.mqtt_subscribe(state_topic)

elif isinstance(self.get_ha_value(entity_id), str) and self.get_ha_value(entity_id) not in attributes.get(
"options", {}
elif (
isinstance(self.get_ha_value(entity_id), str)
and (self.get_ha_value(entity_id) not in attributes.get("options", {}))
and (domain not in ["text", "button"])
):

state = self._state_from_value(self.get_default_config(item))

self.log(f" - Found unexpected str for {entity_id} reverting to default of {state}")
Expand Down Expand Up @@ -1327,14 +1410,15 @@ def _expose_configs(self, over_write=True):
self.log(f" {'Config Item':40s} {'HA Entity':42s} Current State")
self.log(f" {'-----------':40s} {'---------':42s} -------------")

self.ha_entities = {}
for entity_id in self.change_items:
if not "sensor" in entity_id:
self.log(
f" {self.change_items[entity_id]:40s} {entity_id:42s} {self.config_state[self.change_items[entity_id]]}"
)
item = self.change_items[entity_id]
self.log(f" {item:40s} {entity_id:42s} {self.config_state[item]}")
self.handles[entity_id] = self.listen_state(
callback=self.optimise_state_change, entity_id=entity_id
)
self.ha_entities[item] = entity_id

self.mqtt.listen_state(
callback=self.optimise_state_change,
Expand Down Expand Up @@ -1365,7 +1449,10 @@ def optimise_state_change(self, entity_id, attribute, old, new, kwargs):
]:
self._load_pv_system_model()

self.optimise()
if "test" not in item:
self.optimise()
elif "button" in item:
self._run_test()

def _value_from_state(self, state):
value = None
Expand Down Expand Up @@ -1718,7 +1805,7 @@ def optimise(self):

if self.charge_power > 0:
if not status["charge"]["active"]:
start = pd.Timestamp.now()
start = pd.Timestamp.now(tz=self.tz)
else:
start = None

Expand All @@ -1737,7 +1824,7 @@ def optimise(self):

elif self.charge_power < 0:
if not status["discharge"]["active"]:
start = pd.Timestamp.now()
start = pd.Timestamp.now(tz=self.tz)
else:
start = None

Expand Down Expand Up @@ -1872,8 +1959,8 @@ def _create_windows(self):
self.opt["period"] = (self.opt["forced"].diff() > 0).cumsum()
if (self.opt["forced"] != 0).sum() > 0:
x = self.opt[self.opt["forced"] > 0].copy()
x["start"] = x.index
x["end"] = x.index + pd.Timedelta(30, "minutes")
x["start"] = x.index.tz_convert(self.tz)
x["end"] = x.index.tz_convert(self.tz) + pd.Timedelta(30, "minutes")
x["soc"] = x["soc"].round(0).astype(int)
x["soc_end"] = x["soc_end"].round(0).astype(int)
windows = pd.concat(
Expand All @@ -1885,8 +1972,8 @@ def _create_windows(self):
)

x = self.opt[self.opt["forced"] < 0].copy()
x["start"] = x.index
x["end"] = x.index + pd.Timedelta(30, "minutes")
x["start"] = x.index.tz_convert(self.tz)
x["end"] = x.index.tz_convert(self.tz) + pd.Timedelta(30, "minutes")
self.windows = pd.concat(
[
x.groupby("period").first()[["start", "soc", "forced"]],
Expand Down Expand Up @@ -1921,8 +2008,8 @@ def _create_windows(self):

self.charge_power = self.windows["forced"].iloc[0]
self.charge_current = self.charge_power / self.get_config("battery_voltage", default=50)
self.charge_start_datetime = self.windows["start"].iloc[0]
self.charge_end_datetime = self.windows["end"].iloc[0]
self.charge_start_datetime = self.windows["start"].iloc[0].tz_convert(self.tz)
self.charge_end_datetime = self.windows["end"].iloc[0].tz_convert(self.tz)
self.charge_target_soc = self.windows["soc_end"].iloc[0]
self.hold = [
{
Expand All @@ -1937,8 +2024,8 @@ def _create_windows(self):
self.charge_current = 0
self.charge_power = 0
self.charge_target_soc = 0
self.charge_start_datetime = self.static.index[0]
self.charge_end_datetime = self.static.index[0]
self.charge_start_datetime = self.static.index[0].tz_convert(self.tz)
self.charge_end_datetime = self.static.index[0].tz_convert(self.tz)
self.hold = []
self.windows = pd.DataFrame()

Expand Down Expand Up @@ -2659,7 +2746,7 @@ def get_state_retry(self, *args, **kwargs):
retries += 1
self.rlog(
f" - Retrieved invalid state of {state} for {kwargs.get('entity_id', None)} (Attempt {retries} of {GET_STATE_RETRIES})",
level="WARN",
level="WARNING",
)
time.sleep(GET_STATE_WAIT)

Expand Down
12 changes: 8 additions & 4 deletions apps/pv_opt/pvpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from datetime import datetime

OCTOPUS_PRODUCT_URL = r"https://api.octopus.energy/v1/products/"
TIME_FORMAT = "%d/%m %H:%M"
TIME_FORMAT = "%d/%m %H:%M %Z"
MAX_ITERS = 3

AGILE_FACTORS = {
Expand Down Expand Up @@ -69,7 +69,7 @@ def __init__(
self.tz = "GB"
else:
self.log = host.log
self.tz = self.host.tz
self.tz = host.tz

self.export = export
self.eco7 = eco7
Expand Down Expand Up @@ -405,9 +405,11 @@ def __init__(
if self.host:
self.log = host.log
self.rlog = host.rlog
self.tz = host.tz
else:
self.log = print
self.rlog = print
self.tz = "GB"

if imp is None and octopus_account is None:
raise ValueError("Either a named import tariff or Octopus Account details much be provided")
Expand Down Expand Up @@ -504,8 +506,10 @@ def __init__(self, name: str, inverter: InverterModel, battery: BatteryModel, ho
self.host = host
if host:
self.log = host.log
self.tz = host.tz
else:
self.log = print
self.tz = "GB"

def __str__(self):
pass
Expand Down Expand Up @@ -705,7 +709,7 @@ def optimised_force(self, initial_soc, static_flows, contract: Contract, **kwarg
x = x[x["soc_end"] <= 97]

search_window = x.index
str_log = f"{max_slot.strftime(TIME_FORMAT)}: {round_trip_energy_required:5.2f} kWh at {max_import_cost:6.2f}p. "
str_log = f"{max_slot.tz_convert(self.tz).strftime(TIME_FORMAT)}: {round_trip_energy_required:5.2f} kWh at {max_import_cost:6.2f}p. "
if len(search_window) > 0:
# str_log += f"Window: [{search_window[0].strftime(TIME_FORMAT)}-{search_window[-1].strftime(TIME_FORMAT)}] "
pass
Expand All @@ -720,7 +724,7 @@ def optimised_force(self, initial_soc, static_flows, contract: Contract, **kwarg

cost_at_min_price = round_trip_energy_required * min_price

str_log += f"<==> {start_window.strftime(TIME_FORMAT)}: {min_price:5.2f}p/kWh {cost_at_min_price:5.2f}p "
str_log += f"<==> {start_window.tz_convert(self.tz).strftime(TIME_FORMAT)}: {min_price:5.2f}p/kWh {cost_at_min_price:5.2f}p "
str_log += f" SOC: {x.loc[window[0]]['soc']:5.1f}%->{x.loc[window[-1]]['soc_end']:5.1f}% "
factors = []
for slot in window:
Expand Down
2 changes: 1 addition & 1 deletion apps/pv_opt/solax.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def control_charge(self, enable, **kwargs):
self.host.set_select("charge_start_time_2", start)
self.host.set_select("charge_end_time_2", end)

power = self.kwargs.get("power")
power = kwargs.get("power")
if power is not None:
entity_id = self.host.config[f"id_max_charge_current"]
current = abs(round(power / self.host.get_config("battery_voltage"), 1))
Expand Down
17 changes: 17 additions & 0 deletions pvopt_test_card.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type: entities
entities:
- entity: select.pvopt_test_function
name: Function
- entity: select.pvopt_test_enable
name: Enable / Disable
- entity: text.pvopt_test_start
name: Start Time (Local Time Zone)
- entity: text.pvopt_test_end
name: End Time (Local Time Zone)
- entity: number.pvopt_test_power
name: Power
- entity: number.pvopt_test_target_soc
name: Target SOC
- entity: button.pvopt_test_button
name: Send to Inverter
title: PV Opt Test Card

0 comments on commit 06e6528

Please sign in to comment.