Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: I need support
url: https://github.com/nathanmarlor/foxess_modbus/discussions/new?category=support
about: If you're having trouble making something work.
- name: I've got a question
url: https://github.com/nathanmarlor/foxess_modbus/discussions/new?category=q-a
about: If you've got a question, need more information, or you're not sure about something.
- name: Feature request
url: https://github.com/nathanmarlor/foxess_modbus/discussions/new?category=ideas
about: Got a suggestion for a new feature?
- name: Anything else
url: https://github.com/nathanmarlor/foxess_modbus/discussions/new
about: If it's something else or you're not sure, open a discussion.
28 changes: 16 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@ repos:
language: system
types: [python]
require_serial: true
- id: flake8
name: flake8
entry: flake8
language: system
types: [python]
require_serial: true
- id: reorder-python-imports
name: Reorder python imports
entry: reorder-python-imports
language: system
types: [python]
args: [--application-directories=custom_components]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.2.1
hooks:
- id: prettier
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.270
hooks:
- id: ruff
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
# These are duplicated from requirements.txt
additional_dependencies:
[
homeassistant-stubs==2023.6.1,
types-python-slugify==8.0.0.2,
voluptuous-stubs==0.1.1,
]
37 changes: 9 additions & 28 deletions custom_components/foxess_em/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):

for platform in PLATFORMS:
if entry.options.get(platform, True):
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, platform))

if len(entry.options) > 0:
# overwrite data with options
Expand Down Expand Up @@ -104,9 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
peak_utils = PeakPeriodUtils(eco_start_time, eco_end_time)

forecast_controller = ForecastController(hass, solcast_client)
average_controller = AverageController(
hass, eco_start_time, eco_end_time, house_power, aux_power
)
average_controller = AverageController(hass, eco_start_time, eco_end_time, house_power, aux_power)
schedule = Schedule(hass)
battery_controller = BatteryController(
hass,
Expand All @@ -125,9 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
_LOGGER.debug(f"Initialising {connection_type} service")
if connection_type == FOX_CLOUD:
cloud_client = FoxCloudApiClient(session, fox_username, fox_password)
fox_service = FoxCloudService(
hass, cloud_client, eco_start_time, eco_end_time, user_min_soc
)
fox_service = FoxCloudService(hass, cloud_client, eco_start_time, eco_end_time, user_min_soc)
else:
params = {CONNECTION_TYPE: connection_type}
if connection_type == FOX_MODBUS_TCP:
Expand Down Expand Up @@ -176,22 +170,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
forecast_controller.add_update_listener(battery_controller)
average_controller.add_update_listener(battery_controller)

hass.services.async_register(
DOMAIN, "start_force_charge_now", fox_service.start_force_charge_now
)
hass.services.async_register(
DOMAIN, "start_force_charge_off_peak", fox_service.start_force_charge_off_peak
)
hass.services.async_register(
DOMAIN, "stop_force_charge", fox_service.stop_force_charge
)
hass.services.async_register(
DOMAIN, "clear_schedule", battery_controller.clear_schedule
)
hass.services.async_register(DOMAIN, "start_force_charge_now", fox_service.start_force_charge_now)
hass.services.async_register(DOMAIN, "start_force_charge_off_peak", fox_service.start_force_charge_off_peak)
hass.services.async_register(DOMAIN, "stop_force_charge", fox_service.stop_force_charge)
hass.services.async_register(DOMAIN, "clear_schedule", battery_controller.clear_schedule)

hass.data[DOMAIN][entry.entry_id]["unload"] = entry.add_update_listener(
async_reload_entry
)
hass.data[DOMAIN][entry.entry_id]["unload"] = entry.add_update_listener(async_reload_entry)

return True

Expand All @@ -200,10 +184,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
unloaded = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
]
*[hass.config_entries.async_forward_entry_unload(entry, platform) for platform in PLATFORMS]
)
)

Expand Down
10 changes: 4 additions & 6 deletions custom_components/foxess_em/average/average_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from datetime import time
from datetime import timedelta

from custom_components.foxess_em.common.hass_load_controller import HassLoadController
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_utc_time_change
from pandas import DataFrame

from custom_components.foxess_em.common.hass_load_controller import HassLoadController

from ..average.average_model import AverageModel
from ..common.callback_controller import CallbackController
from ..common.unload_controller import UnloadController
Expand All @@ -30,15 +31,12 @@ def __init__(
aux_power: list[str],
) -> None:
self._hass = hass
self._last_update = None
self._last_update = datetime.min

entities = {
"house_load_7d": TrackedSensor(
HistorySensor(house_power, timedelta(days=2), False),
[
HistorySensor(sensor, timedelta(days=2), False)
for sensor in aux_power
],
[HistorySensor(sensor, timedelta(days=2), False) for sensor in aux_power],
)
}

Expand Down
7 changes: 1 addition & 6 deletions custom_components/foxess_em/average/average_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,7 @@ async def _update_item(self, item: HistorySensor) -> None:
to_date = datetime.now().astimezone()

if item.whole_day:
to_date = (
datetime.now()
.astimezone()
.replace(hour=0, minute=0, second=0, microsecond=0)
.astimezone(tz.UTC)
)
to_date = datetime.now().astimezone().replace(hour=0, minute=0, second=0, microsecond=0).astimezone(tz.UTC)

from_date = to_date - item.period

Expand Down
12 changes: 3 additions & 9 deletions custom_components/foxess_em/battery/battery_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ def __init__(
HassLoadController.__init__(self, hass, self.async_refresh)

# Refresh on SoC change
battery_refresh = async_track_state_change(
self._hass, battery_soc, self.refresh
)
battery_refresh = async_track_state_change(self._hass, battery_soc, self.refresh)
self._unload_listeners.append(battery_refresh)

def ready(self) -> bool:
Expand Down Expand Up @@ -114,9 +112,7 @@ def get_schedule(self, start: datetime = None, end: datetime = None):
return schedule

return {
k: v
for k, v in schedule.items()
if datetime.fromisoformat(k) > start and datetime.fromisoformat(k) < end
k: v for k, v in schedule.items() if datetime.fromisoformat(k) > start and datetime.fromisoformat(k) < end
}

def raw_data(self):
Expand Down Expand Up @@ -178,9 +174,7 @@ def forecast_last_update_str(self) -> str:

def set_boost(self, value: float) -> None:
"""Set boost on/off"""
self._schedule.upsert(
self._peak_utils.next_eco_start(), {"boost_status": value}
)
self._schedule.upsert(self._peak_utils.next_eco_start(), {"boost_status": value})
self.refresh()

def get_boost(self) -> bool:
Expand Down
57 changes: 15 additions & 42 deletions custom_components/foxess_em/battery/battery_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
from datetime import timedelta

import pandas as pd
from homeassistant.core import HomeAssistant

from custom_components.foxess_em.battery.battery_util import BatteryUtils
from custom_components.foxess_em.battery.schedule import Schedule
from custom_components.foxess_em.util.peak_period_util import PeakPeriodUtils
from homeassistant.core import HomeAssistant

from ..util.exceptions import NoDataError

Expand Down Expand Up @@ -53,9 +54,7 @@ def raw_data(self):
"""Return raw data in dictionary form"""
now = datetime.now().astimezone()

filtered = self._model[
["period_start", "pv_estimate", "load", "battery", "grid"]
]
filtered = self._model[["period_start", "pv_estimate", "load", "battery", "grid"]]

filtered = filtered.set_index("period_start").resample("5Min").mean()
filtered["period_start"] = pd.to_datetime(filtered.index.values, utc=True)
Expand Down Expand Up @@ -95,15 +94,11 @@ def refresh_battery_model(self, forecast: pd.DataFrame, load: pd.DataFrame) -> N
_, min_soc = self._charge_totals(load_forecast, now, battery)

for index, _ in future.iterrows():
period = (
load_forecast.iloc[index]["period_start"].to_pydatetime().astimezone()
)
period = load_forecast.iloc[index]["period_start"].to_pydatetime().astimezone()
if period.time() == self._eco_start_time:
# landed on the start of the eco period
boost = self._get_total_additional_charge(period)
total, min_soc = self._charge_totals(
load_forecast, period, battery, boost
)
total, min_soc = self._charge_totals(load_forecast, period, battery, boost)
battery += total
elif self._peak_utils.in_peak(period.time()) and battery < min_soc:
# hold SoC in off-peak period
Expand All @@ -121,9 +116,7 @@ def refresh_battery_model(self, forecast: pd.DataFrame, load: pd.DataFrame) -> N
load_forecast.at[index, "battery"] = battery

for index, _ in future.iterrows():
period = (
load_forecast.iloc[index]["period_start"].to_pydatetime().astimezone()
)
period = load_forecast.iloc[index]["period_start"].to_pydatetime().astimezone()
if period.time() == self._eco_start_time:
self._add_metadata(load_forecast, period)

Expand All @@ -148,26 +141,17 @@ def _charge_totals(
eco_end_time = self._peak_utils.next_eco_end(eco_start)
next_eco_start = eco_start + timedelta(days=1)
# grab all peak values
peak = model[
(model["period_start"] > eco_end_time)
& (model["period_start"] < next_eco_start)
]
peak = model[(model["period_start"] > eco_end_time) & (model["period_start"] < next_eco_start)]

# sum forecast and house load
forecast_sum = peak.pv_estimate.sum()
load_sum = peak.load.sum()
dawn_load = self._dawn_load(model, eco_end_time)
dawn_charge = self._dawn_charge_needs(dawn_load)
day_charge = self._day_charge_needs(forecast_sum, load_sum)
max_charge = self._battery_utils.ceiling_charge_total(
max([dawn_charge, day_charge])
)
max_charge = self._battery_utils.ceiling_charge_total(max([dawn_charge, day_charge]))
min_soc = (
max_charge
if boost == 0
else self._battery_utils.ceiling_charge_total(
max([battery, max_charge]) + boost
)
max_charge if boost == 0 else self._battery_utils.ceiling_charge_total(max([battery, max_charge]) + boost)
)
total = self._battery_utils.ceiling_charge_total(max([0, min_soc - battery]))
# store in dataframe for retrieval later
Expand Down Expand Up @@ -200,10 +184,7 @@ def _add_metadata(self, model: pd.DataFrame, period: datetime):
eco_end_time = self._peak_utils.next_eco_end(eco_start)
next_eco_start = eco_start + timedelta(days=1)
# grab all peak values
peak = model[
(model["period_start"] > eco_end_time)
& (model["period_start"] < next_eco_start)
]
peak = model[(model["period_start"] > eco_end_time) & (model["period_start"] < next_eco_start)]

# metadata - import/export
grid_import = abs(peak[(peak["grid"] < 0)].grid.sum())
Expand Down Expand Up @@ -239,8 +220,7 @@ def battery_depleted_time(self) -> datetime:
return None

battery_depleted = self._model[
(self._model["battery"] == 0)
& (self._model["period_start"] > datetime.now().astimezone())
(self._model["battery"] == 0) & (self._model["period_start"] > datetime.now().astimezone())
]

if len(battery_depleted) == 0:
Expand All @@ -255,9 +235,7 @@ def peak_grid_import(self) -> float:
eco_start = self._peak_utils.next_eco_start()

grid_use = self._model[
(self._model["grid"] < 0)
& (self._model["period_start"] > now)
& (self._model["period_start"] < eco_start)
(self._model["grid"] < 0) & (self._model["period_start"] > now) & (self._model["period_start"] < eco_start)
]

if len(grid_use) == 0:
Expand All @@ -271,9 +249,7 @@ def peak_grid_export(self) -> float:
eco_start = self._peak_utils.next_eco_start()

grid_export = self._model[
(self._model["grid"] > 0)
& (self._model["period_start"] > now)
& (self._model["period_start"] < eco_start)
(self._model["grid"] > 0) & (self._model["period_start"] > now) & (self._model["period_start"] < eco_start)
]

if len(grid_export) == 0:
Expand All @@ -297,8 +273,7 @@ def _battery_capacity_remaining(self) -> float:
def _update_model_forecasts(self, future: pd.DataFrame, now: datetime):
# keep original values including load, pv, grid etc
hist = self._model[
(self._model["period_start"] <= now)
& (self._model["period_start"] > (now - timedelta(days=3)))
(self._model["period_start"] <= now) & (self._model["period_start"] > (now - timedelta(days=3)))
]
# set global model
return pd.concat([hist, future])
Expand Down Expand Up @@ -339,9 +314,7 @@ def _dawn_load(self, model: pd.DataFrame, eco_end_time: datetime) -> float:
"""Dawn load"""
dawn_time = self._dawn_time(model, eco_end_time)

dawn_load = model[
(model["period_start"] > eco_end_time) & (model["period_start"] < dawn_time)
]
dawn_load = model[(model["period_start"] > eco_end_time) & (model["period_start"] < dawn_time)]

load_sum = abs(dawn_load.delta.sum())

Expand Down
Loading