Skip to content

Commit

Permalink
Merge pull request #49 from fboundy/3.2.1
Browse files Browse the repository at this point in the history
3.2.1
  • Loading branch information
fboundy authored Dec 14, 2023
2 parents 84f74e4 + 94ab003 commit cfd9a23
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 71 deletions.
3 changes: 3 additions & 0 deletions apps/pv_opt/inverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"maximum_dod_percent": "number.solis_battery_minimum_soc",
"id_battery_soc": "sensor.solis_battery_soc",
"id_consumption": ["sensor.solis_house_load", "sensor.solis_bypass_load"],
"id_consumption_today": "sensor.solis_house_load_today",
"id_grid_import_today": "sensor.solis_grid_import_today",
"id_grid_export_today": "sensor.solis_grid_export_today",
"id_grid_import_power": "sensor.solis_grid_import_power",
"id_grid_export_power": "sensor.solis_grid_export_power",
"id_battery_charge_power": "sensor.solis_battery_input_energy",
Expand Down
127 changes: 95 additions & 32 deletions apps/pv_opt/pv_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#
USE_TARIFF = True

VERSION = "3.2.0"
VERSION = "3.2.1"

DATE_TIME_FORMAT_LONG = "%Y-%m-%d %H:%M:%S%z"
DATE_TIME_FORMAT_SHORT = "%d-%b %H:%M"
Expand Down Expand Up @@ -303,28 +303,55 @@ def _cost_today(self):
freq="30T",
)
if (
"id_grid_import_today" in self.config
and "id_grid_export_today" in self.config
):
cols = ["grid_import", "grid_export"]
grid = (
pd.concat(
[
self.hass2df(self.config[f"id_{col}_today"], days=1)
.astype(float)
.resample("30T")
.ffill()
.reindex(index)
.ffill()
for col in cols
],
axis=1,
)
.set_axis(cols, axis=1)
.loc[pd.Timestamp.now(tz="UTC").normalize() :]
)
grid = (-grid.diff(-1).clip(upper=0) * 2000).round(0)[:-1].fillna(0)

elif (
"id_grid_import_power" in self.config
and "id_grid_export_power" in self.config
):
grid = (
(
self.hass2df(self.config["id_grid_import_power"], days=1)
.astype(float)
.resample("30T")
.mean()
.reindex(index)
.fillna(0)
.reindex(index)
pd.concat(
[
self.hass2df(self.config["id_grid_import_power"], days=1)
.astype(float)
.resample("30T")
.mean()
.reindex(index)
.fillna(0)
.reindex(index),
self.hass2df(self.config["id_grid_export_power"], days=1)
.astype(float)
.resample("30T")
.mean()
.reindex(index)
.fillna(0),
],
axis=1,
)
- (
self.hass2df(self.config["id_grid_export_power"], days=1)
.astype(float)
.resample("30T")
.mean()
.reindex(index)
.fillna(0)
)
).loc[pd.Timestamp.now(tz="UTC").normalize() :]
.set_axis(["grid_import", "grid_export"], axis=1)
.loc[pd.Timestamp.now(tz="UTC").normalize() :]
)

elif "id_grid_power" in self.config:
grid = (
-(
Expand All @@ -338,9 +365,8 @@ def _cost_today(self):
)
).loc[pd.Timestamp.now(tz="UTC").normalize() :]

# self.log(">>>")
cost_today = self.contract.net_cost(grid_flow=grid).sum()
# self.log(cost_today)
# self.log(grid)
cost_today = self.contract.net_cost(grid_flow=grid)
return cost_today

@ad.app_lock
Expand Down Expand Up @@ -1152,11 +1178,15 @@ def optimise(self):
f"Plan time: {self.static.index[0].strftime('%d-%b %H:%M')} - {self.static.index[-1].strftime('%d-%b %H:%M')} Initial SOC: {self.initial_soc} Base Cost: {self.base_cost.sum():5.2f} Opt Cost: {self.opt_cost.sum():5.2f}"
)
self.log("")
self.log(
f"Optimiser elapsed time {(pd.Timestamp.now()- self.t0).total_seconds():0.2f} seconds"
)
optimiser_elapsed = round((pd.Timestamp.now() - self.t0).total_seconds(), 1)
self.log(f"Optimiser elapsed time {optimiser_elapsed:0.1f} seconds")
self.log("")
self.log("")
self.write_to_hass(
entity=f"sensor.{self.prefix}_optimiser_elapsed",
state=optimiser_elapsed,
attributes={"state_class": "measurement"},
)

self._status("Writing to HA")
self._write_output()
Expand Down Expand Up @@ -1313,6 +1343,19 @@ def _log_inverter_status(self, status):
self.log("")

def write_to_hass(self, entity, state, attributes):
if not self.entity_exists(entity_id=entity):
self.log(f"Creating HA Entity {entity}")
id = entity.replace("sensor.", "")
conf = {
"state_topic": f"homeassistant/sensor/{id}/state",
"command_topic": f"homeassistant/domain/{id}/set",
"optimistic": True,
"unique_id": id,
}

conf_topic = f"homeassistant/self/{id}/config"
self.mqtt.mqtt_publish(conf_topic, dumps(conf), retain=True)

try:
self.my_entity = self.get_entity(entity)
self.my_entity.set_state(state=state, attributes=attributes)
Expand All @@ -1325,34 +1368,40 @@ def write_to_hass(self, entity, state, attributes):
def write_cost(self, name, entity, cost, df):
cost_today = self._cost_today()
midnight = pd.Timestamp.now(tz="UTC").normalize() + pd.Timedelta("24H")
# self.log(
# f">>> {cost.loc[:midnight].sum():0.0f} {cost.loc[midnight:].sum():0.0f} {cost.sum():0.0f} {cost.loc[midnight]:0.0f}"
# )
df = df.fillna(0).round(2)
df["period_start"] = (
df.index.tz_convert(self.tz).strftime("%Y-%m-%dT%H:%M:%S%z").str[:-2]
+ ":00"
)
cols = ["soc", "forced", "import", "export", "grid", "consumption"]

cost = pd.DataFrame(pd.concat([cost_today, cost])).set_axis(["cost"], axis=1)
cost["cumulative_cost"] = cost["cost"].cumsum()

for d in [df, cost]:
d["period_start"] = (
d.index.tz_convert(self.tz).strftime("%Y-%m-%dT%H:%M:%S%z").str[:-2]
+ ":00"
)

self.write_to_hass(
entity=entity,
state=round((cost.sum() + cost_today) / 100, 2),
state=round((cost["cost"].sum()) / 100, 2),
attributes={
"friendly_name": name,
"unit_of_measurement": "GBP",
"cost_today": round(
(cost.loc[: midnight - pd.Timedelta("30T")].sum() + cost_today)
/ 100,
(cost["cost"].loc[: midnight - pd.Timedelta("30T")].sum()) / 100,
2,
),
"cost_tomorrow": round((cost.loc[midnight:].sum()) / 100, 2),
"cost_tomorrow": round((cost["cost"].loc[midnight:].sum()) / 100, 2),
}
| {
col: df[["period_start", col]].to_dict("records")
for col in cols
if col in df.columns
},
}
| {"cost": cost[["period_start", "cumulative_cost"]].to_dict("records")},
)

def _write_output(self):
Expand Down Expand Up @@ -1404,6 +1453,20 @@ def _write_output(self):
},
)

for offset in [1, 4, 8, 12]:
loc = pd.Timestamp.now(tz="UTC") + pd.Timedelta(f"{offset}H")
locs = [loc.floor("30T"), loc.ceil("30T")]
socs = [self.opt.loc[l]["soc"] for l in locs]
soc = (loc - locs[0]) / (locs[1] - locs[0]) * (socs[1] - socs[0]) + socs[0]
entity_id = f"sensor.{self.prefix}_soc_h{offset}"
attributes = {
"friendly_name": f"PV Opt Predicted SOC ({offset} hour delay)",
"unit_of_measurement": "%",
"state_class": "measurement",
"device_class": "battery",
}
self.write_to_hass(entity=entity_id, state=soc, attributes=attributes)

def load_solcast(self):
if self.debug:
self.log("Getting Solcast data")
Expand Down
56 changes: 17 additions & 39 deletions apps/pv_opt/pvpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,28 +160,15 @@ def to_df(self, start=None, end=None):
if "AGILE" in self.name:
if self.day_ahead is not None and df.index[-1].day == end.day:
# reset the day ahead forecasts if we've got a forecast going into tomorrow
# self.log(">>>Resetting day ahead prices")
self.day_ahead = None

if pd.Timestamp.now().hour > 11 and df.index[-1].day != end.day:
# if it is after 11 but we don't have new Agile prices yet, check for a day-ahead forecast
# self.log(">>>Checking for day ahead prices")
if self.day_ahead is None:
self.day_ahead = self.get_day_ahead(df.index[0])
# if self.day_ahead is not None:
# str_log = f">>>Downloaded Day Ahead prices OK. "
# str_log += f"Date range: {self.day_ahead.index[0].strftime(TIME_FORMAT)}"
# str_log += (
# f" - {self.day_ahead.index[-1].strftime(TIME_FORMAT)}"
# )
# self.log(str_log)
# else:
# self.log(">>>Failed to get Day Ahead prices OK")

if self.day_ahead is not None:
self.day_ahead = self.day_ahead.sort_index()
# str_log = ">>>Predicting Agile prices from Day Ahead: "
# str_log += f"End before: {df.index[-1].strftime(TIME_FORMAT)} "
mask = (self.day_ahead.index.hour >= 16) & (
self.day_ahead.index.hour < 19
)
Expand All @@ -196,27 +183,8 @@ def to_df(self, start=None, end=None):
.loc[df.index[-1] :]
.iloc[1:]
)
# agile = self.day_ahead.loc[df.index[-1:]].iloc[1:]

# agile = self.day_ahead.loc[df.index[-1:]].iloc[1:].copy()
# mask = (agile.index.hour >= 16) & (agile.index.hour < 19)
# agile[mask] = agile[mask] * 0.186 + 16.5
# agile[~mask] = agile[~mask] * 0.229 - 0.6

# self.log(f">>{df.index}")
# self.log(f">>>{agile.index}")

df = pd.concat([df, agile])
# str_log += f"End after: {df.index[-1].strftime(TIME_FORMAT)} "
# self.log(str_log)
# else:
# # str_log = ">>> No Day Ahead checks: "
# if self.day_ahead is None:
# str_log += "self.day_ahead is NULL, "
# if pd.Timestamp.now().hour > 11:
# str_log += "hour > 11, "
# str_log += f"Index end day: {df.index[-1].day} End day: {end.day}"
# self.log(str_log)

# If the index frequency >30 minutes so we need to just extend it:
if (
Expand Down Expand Up @@ -313,7 +281,6 @@ def get_day_ahead(self, start):
price.index = price.index.tz_localize("CET")
price.index = price.index.tz_convert("UTC")
price = price[~price.index.duplicated()]
# self.log(f">>> Duplicates: {price.index.has_duplicates}")
return price.resample("30T").ffill().loc[start:]


Expand Down Expand Up @@ -436,13 +403,25 @@ def __str__(self):
str += f"{tariff.__str__()}\n"
return str

def net_cost(self, grid_flow, grid_col="grid"):
def net_cost(self, grid_flow, **kwargs):
grid_import = kwargs.get("grid_import", "grid_import")
grid_export = kwargs.get("grid_export", "grid_export")
grid_col = kwargs.get("grid_col", "grid")
start = grid_flow.index[0]
end = grid_flow.index[-1]
if isinstance(grid_flow, pd.DataFrame):
grid_flow = grid_flow[grid_col]
grid_imp = grid_flow.clip(0)
grid_exp = grid_flow.clip(upper=0)
if (
isinstance(grid_flow, pd.DataFrame)
and (grid_export in grid_flow.columns)
and (grid_import in grid_flow.columns)
):
grid_imp = grid_flow[grid_import]
grid_exp = grid_flow[grid_export]
else:
if isinstance(grid_flow, pd.DataFrame):
grid_flow = grid_flow[grid_col]

grid_imp = grid_flow.clip(0)
grid_exp = grid_flow.clip(upper=0)

nc = self.imp.to_df(start, end)["fixed"]
nc += self.imp.to_df(start, end)["unit"] * grid_imp / 2000
Expand Down Expand Up @@ -727,7 +706,6 @@ def optimised_force(self, initial_soc, static_flows, contract: Contract, **kwarg
else:
available[max_slot] = False
else:
# self.log(f">>>{str_log}")
done = True
else:
done = True
Expand Down

0 comments on commit cfd9a23

Please sign in to comment.