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

Schedule housekeeping, filtering and readme update #113

Merged
merged 6 commits into from
Dec 7, 2022
Merged
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
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,12 @@ Description of sensors:

Notes:

- a negative capacity value indicates surplus charge available
- all capacity values are forward looking to the next period once past the eco-start time</br>

| Sensor | Description | Attributes |
| ---------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Capacity: Battery Empty Time | Forecasted time battery will be depleted (Unknown if battery is empty) | |
| Capacity: Charge Needed | Charge needed for the next off-peak period | Dawn charge needed </br> Day charge needed </br> Target % |
| Capacity: Dawn | Forecasted battery capacity at dawn | |
| Capacity: Eco End | Forecasted battery capacity at the end of the off-peak period | |
| Capacity: Charge Needed | Charge needed for the next off-peak period | Dawn charge needed </br> Day charge needed </br> Min SoC |
| Capacity: Eco Start | Forecasted battery capacity at the start of the off-peak period | |
| Capacity: Next Dawn Time | Forecasted next dawn time (i.e. solar output > house load) | |
| Capacity: Peak Grid Export | Forecasted solar export to grid until the next off-peak period | |
Expand All @@ -128,6 +125,8 @@ Notes:
| Last Update | Last update time | Battery last update</br> Forecast last update</br> Average last update</br> |
| Load: Daily | Total load, averaged over the last 2 complete days | |
| Load: Peak | Peak only load (i.e. outside of the Go period), averaged over the last 2 complete days | |
| FoxESS EM: Schedule | Entity to persist the schedule | Schedule stored as JSON |
| FoxESS EM: Raw Data | Entity to persist the the raw data for graphing purposes | Raw data stored as JSON - disabled by default |

</details>

Expand All @@ -146,6 +145,32 @@ Description of switches:

## Extras

<details>
<summary><b>Graphing</b></summary>

<b>Important! Before following this guide add the following to your configuration.yaml to prevent the HA database becoming bloated</b>

```
recorder:
exclude:
entities:
- sensor.foxess_em_raw_data
```

- Enable the FoxESS Raw Data entity from the entity settings:

![Service](images/raw-data-entity.png)</p>

- Install Apex Charts from HACS
- Use the templated example in the /apex-example folder

![Raw Data Graph](images/raw-data-graph.png)</p>

Dashed = predicted / Solid = actual</br>
Battery = blue / Load = pink / Solar = orange / Grid = green

</details>

<details>
<summary><b>Energy Dashboard Forecast</b></summary>

Expand Down
4 changes: 4 additions & 0 deletions custom_components/foxess_em/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
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
)

return True


Expand Down
8 changes: 5 additions & 3 deletions custom_components/foxess_em/average/average_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ def __init__(
house_power: str,
aux_power: list[str],
) -> None:
UnloadController.__init__(self)
CallbackController.__init__(self)
HassLoadController.__init__(self, hass, self.async_refresh)
self._hass = hass
self._last_update = None

Expand All @@ -47,6 +44,11 @@ def __init__(

self._model = AverageModel(hass, entities, eco_start_time, eco_end_time)

# Setup mixins
UnloadController.__init__(self)
CallbackController.__init__(self)
HassLoadController.__init__(self, hass, self.async_refresh)

# Refresh every hour on the half hour
midnight_refresh = async_track_utc_time_change(
self._hass,
Expand Down
26 changes: 21 additions & 5 deletions custom_components/foxess_em/battery/battery_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ def __init__(
schedule: Schedule,
peak_utils: PeakPeriodUtils,
) -> None:
UnloadController.__init__(self)
CallbackController.__init__(self)
HassLoadController.__init__(self, hass, self.async_refresh)
self._hass = hass
self._schedule = schedule
self._peak_utils = peak_utils
Expand All @@ -60,6 +57,11 @@ def __init__(
self._average_controller = average_controller
self._last_update = None

# Setup mixins
UnloadController.__init__(self)
CallbackController.__init__(self)
HassLoadController.__init__(self, hass, self.async_refresh)

# Refresh on SoC change
battery_refresh = async_track_state_change(
self._hass, battery_soc, self.refresh
Expand Down Expand Up @@ -100,9 +102,18 @@ def charge_to_perc(self) -> int:
"""Calculate percentage target"""
return self._battery_utils.charge_to_perc(self.min_soc())

def get_schedule(self):
def get_schedule(self, start: datetime = None, end: datetime = None):
"""Return charge schedule"""
return self._schedule.get_all()
schedule = self._schedule.get_all()

if start is None or end is None:
return schedule

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

def raw_data(self):
"""Return raw data in dictionary form"""
Expand All @@ -128,6 +139,11 @@ def min_soc(self) -> float:
"""Total kWh required to charge"""
return self._schedule_info()["min_soc"]

def clear_schedule(self, *args) -> None:
"""Clear schedule"""
self._schedule.clear()
self.refresh()

def _schedule_info(self) -> float:
"""Schedule info"""
return self._schedule.get(self._peak_utils.next_eco_start())
Expand Down
1 change: 1 addition & 0 deletions custom_components/foxess_em/battery/battery_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
state_attributes={
"raw_data": "raw_data",
},
enabled=False,
),
"schedule": SensorDescription(
key="empty",
Expand Down
38 changes: 35 additions & 3 deletions custom_components/foxess_em/battery/schedule.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
"""Battery controller"""
import logging
from datetime import datetime
from datetime import timedelta
from typing import Any

from custom_components.foxess_em.common.hass_load_controller import HassLoadController
from custom_components.foxess_em.common.unload_controller import UnloadController
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_utc_time_change

_LOGGER = logging.getLogger(__name__)
_SCHEDULE = "sensor.foxess_em_schedule"


class Schedule(HassLoadController):
class Schedule(HassLoadController, UnloadController):
"""Schedule"""

def __init__(self, hass: HomeAssistant) -> None:
"""Get persisted schedule from states"""
HassLoadController.__init__(self, hass, self.load)
self._hass = hass
self._schedule = {}

def load(self, *args) -> None:
# Setup mixins
UnloadController.__init__(self)
HassLoadController.__init__(self, hass, self.load)

# Housekeeping on schedule
housekeeping = async_track_utc_time_change(
self._hass,
self._housekeeping,
hour=0,
minute=0,
second=10,
local=False,
)
self._unload_listeners.append(housekeeping)

async def load(self, *args) -> None:
"""Load schedule from state"""
schedule = self._hass.states.get(_SCHEDULE)

Expand All @@ -28,6 +45,8 @@ def load(self, *args) -> None:
else:
self._schedule = {}

self._housekeeping()

def upsert(self, index: datetime, params: dict) -> None:
"""Update or insert new item"""
_LOGGER.debug(f"Updating schedule {index}: {params}")
Expand All @@ -50,3 +69,16 @@ def get(self, index: datetime) -> dict[str, Any] | None:
return self._schedule[index]
else:
return None

def clear(self) -> None:
"""Reset all schedule items"""
self._schedule.clear()

def _housekeeping(self, *args) -> None:
"""Clean up schedule"""
two_weeks_ago = datetime.now().astimezone() - timedelta(days=14)

for schedule in list(self._schedule.keys()):
if datetime.fromisoformat(schedule) < two_weeks_ago:
_LOGGER.debug(f"Schedule housekeeping, removing data for {schedule}")
self._schedule.pop(schedule)
2 changes: 1 addition & 1 deletion custom_components/foxess_em/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async def async_get_events(
) -> list[CalendarEvent]:
"""Return all events within a time window"""

events = self._controller.get_schedule()
events = self._controller.get_schedule(start_date, end_date)

calendar_events = []
for key in events:
Expand Down
1 change: 1 addition & 0 deletions custom_components/foxess_em/common/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __init__(
self._attributes = {}
self._attr_extra_state_attributes = {}
self._attr_entity_registry_visible_default = entity_description.visible
self._attr_entity_registry_enabled_default = entity_description.enabled

self._attr_device_info = {
ATTR_IDENTIFIERS: {(DOMAIN, entry.entry_id)},
Expand Down
1 change: 1 addition & 0 deletions custom_components/foxess_em/common/sensor_desc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ class SensorDescription(SensorEntityDescription):
should_poll: bool | None = False
state_attributes: dict | None = field(default_factory=dict)
visible: bool | None = True
enabled: bool | None = True
store_attributes: bool | None = False
2 changes: 1 addition & 1 deletion custom_components/foxess_em/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# Configuration and options
SOLCAST_API_KEY = "key"
SOLCAST_SCAN_INTERVAL = "scan_interval"
# SOLCAST_URL = "https://238beaac-50d9-4aa4-89eb-24f49d786205.mock.pstmn.io"
# SOLCAST_URL = "https://06db0776-e926-42bb-a6a2-85f88da8b0c8.mock.pstmn.io"
SOLCAST_URL = "https://api.solcast.com.au"
FOX_USERNAME = "fox_username"
FOX_PASSWORD = "fox_password"
Expand Down
8 changes: 5 additions & 3 deletions custom_components/foxess_em/forecast/forecast_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ class ForecastController(UnloadController, CallbackController, HassLoadControlle
"""Class to manage forecast retrieval"""

def __init__(self, hass: HomeAssistant, api: SolcastApiClient) -> None:
UnloadController.__init__(self)
CallbackController.__init__(self)
HassLoadController.__init__(self, hass, self.async_refresh)
self._hass = hass
self._api = ForecastModel(api)
self._api_count = 0
self._last_update = None

# Setup mixins
UnloadController.__init__(self)
CallbackController.__init__(self)
HassLoadController.__init__(self, hass, self.async_refresh)

async_call_later(hass, 5, self.setup_refresh)

async def setup_refresh(self, *args):
Expand Down
Binary file added images/raw-data-entity.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/raw-data-graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.