From 4bc07baa8bfb8a7fbcb79faaa4c60c66f7bae183 Mon Sep 17 00:00:00 2001
From: Nathan Marlor
Date: Tue, 6 Dec 2022 08:43:47 +0000
Subject: [PATCH 1/6] Added schedule housekeeping to run at midnight
---
.../foxess_em/battery/schedule.py | 28 ++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/custom_components/foxess_em/battery/schedule.py b/custom_components/foxess_em/battery/schedule.py
index 94957fb..7685178 100755
--- a/custom_components/foxess_em/battery/schedule.py
+++ b/custom_components/foxess_em/battery/schedule.py
@@ -1,24 +1,39 @@
"""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"""
+ UnloadController.__init__(self)
HassLoadController.__init__(self, hass, self.load)
self._hass = hass
self._schedule = {}
+ # 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)
+
def load(self, *args) -> None:
"""Load schedule from state"""
schedule = self._hass.states.get(_SCHEDULE)
@@ -28,6 +43,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}")
@@ -50,3 +67,12 @@ def get(self, index: datetime) -> dict[str, Any] | None:
return self._schedule[index]
else:
return None
+
+ 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)
From 8b53f262f56b98bea00155c9bf1fcf85bfab46e6 Mon Sep 17 00:00:00 2001
From: Nathan Marlor
Date: Tue, 6 Dec 2022 08:48:33 +0000
Subject: [PATCH 2/6] Added service to clear schedule in case we run into
trouble with persisted values
---
custom_components/foxess_em/__init__.py | 3 +++
custom_components/foxess_em/battery/battery_controller.py | 5 +++++
custom_components/foxess_em/battery/schedule.py | 4 ++++
3 files changed, 12 insertions(+)
diff --git a/custom_components/foxess_em/__init__.py b/custom_components/foxess_em/__init__.py
index 559c404..1665a0a 100755
--- a/custom_components/foxess_em/__init__.py
+++ b/custom_components/foxess_em/__init__.py
@@ -133,6 +133,9 @@ 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
diff --git a/custom_components/foxess_em/battery/battery_controller.py b/custom_components/foxess_em/battery/battery_controller.py
index 50ae02c..859efda 100755
--- a/custom_components/foxess_em/battery/battery_controller.py
+++ b/custom_components/foxess_em/battery/battery_controller.py
@@ -128,6 +128,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())
diff --git a/custom_components/foxess_em/battery/schedule.py b/custom_components/foxess_em/battery/schedule.py
index 7685178..55c8138 100755
--- a/custom_components/foxess_em/battery/schedule.py
+++ b/custom_components/foxess_em/battery/schedule.py
@@ -68,6 +68,10 @@ def get(self, index: datetime) -> dict[str, Any] | None:
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)
From 9464071da6b369d19a1e2cfb1e0db05e1b52c6f6 Mon Sep 17 00:00:00 2001
From: Nathan Marlor
Date: Tue, 6 Dec 2022 08:55:15 +0000
Subject: [PATCH 3/6] Added filtering to the calendar to reduce data retrieval
---
.../foxess_em/battery/battery_controller.py | 13 +++++++++++--
custom_components/foxess_em/calendar.py | 2 +-
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/custom_components/foxess_em/battery/battery_controller.py b/custom_components/foxess_em/battery/battery_controller.py
index 859efda..16a2093 100755
--- a/custom_components/foxess_em/battery/battery_controller.py
+++ b/custom_components/foxess_em/battery/battery_controller.py
@@ -100,9 +100,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"""
diff --git a/custom_components/foxess_em/calendar.py b/custom_components/foxess_em/calendar.py
index 104c85c..6f613a4 100755
--- a/custom_components/foxess_em/calendar.py
+++ b/custom_components/foxess_em/calendar.py
@@ -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:
From b3b906b60868d1130b2158e6cac5ac308402e227 Mon Sep 17 00:00:00 2001
From: Nathan Marlor
Date: Tue, 6 Dec 2022 09:07:40 +0000
Subject: [PATCH 4/6] Disable raw data by default
---
custom_components/foxess_em/battery/battery_sensor.py | 1 +
custom_components/foxess_em/common/sensor.py | 1 +
custom_components/foxess_em/common/sensor_desc.py | 1 +
3 files changed, 3 insertions(+)
diff --git a/custom_components/foxess_em/battery/battery_sensor.py b/custom_components/foxess_em/battery/battery_sensor.py
index 52bf695..8cc994c 100755
--- a/custom_components/foxess_em/battery/battery_sensor.py
+++ b/custom_components/foxess_em/battery/battery_sensor.py
@@ -89,6 +89,7 @@
state_attributes={
"raw_data": "raw_data",
},
+ enabled=False,
),
"schedule": SensorDescription(
key="empty",
diff --git a/custom_components/foxess_em/common/sensor.py b/custom_components/foxess_em/common/sensor.py
index 61e1a11..d3eb4ca 100755
--- a/custom_components/foxess_em/common/sensor.py
+++ b/custom_components/foxess_em/common/sensor.py
@@ -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)},
diff --git a/custom_components/foxess_em/common/sensor_desc.py b/custom_components/foxess_em/common/sensor_desc.py
index 780395d..aeefad2 100755
--- a/custom_components/foxess_em/common/sensor_desc.py
+++ b/custom_components/foxess_em/common/sensor_desc.py
@@ -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
From f2ed5de6c7746dca8d64a5316af6db856142ae82 Mon Sep 17 00:00:00 2001
From: Nathan Marlor
Date: Tue, 6 Dec 2022 09:23:00 +0000
Subject: [PATCH 5/6] Updated readme
---
README.md | 33 +++++++++++++++++++++++++++++----
images/raw-data-entity.png | Bin 0 -> 71278 bytes
images/raw-data-graph.png | Bin 0 -> 138055 bytes
3 files changed, 29 insertions(+), 4 deletions(-)
create mode 100644 images/raw-data-entity.png
create mode 100644 images/raw-data-graph.png
diff --git a/README.md b/README.md
index 031bc97..d0cc0c8 100755
--- a/README.md
+++ b/README.md
@@ -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
| 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 Day charge needed 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 Day charge needed 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 | |
@@ -128,6 +125,8 @@ Notes:
| Last Update | Last update time | Battery last update Forecast last update Average last update |
| 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 |
@@ -146,6 +145,32 @@ Description of switches:
## Extras
+
+ Graphing
+
+Important! Before following this guide add the following to your configuration.yaml to prevent the HA database becoming bloated
+
+```
+recorder:
+ exclude:
+ entities:
+ - sensor.foxess_em_raw_data
+```
+
+- Enable the FoxESS Raw Data entity from the entity settings:
+
+![Service](images/raw-data-entity.png)
+
+- Install Apex Charts from HACS
+- Use the templated example in the /apex-example folder
+
+![Raw Data Graph](images/raw-data-graph.png)
+
+Dashed = predicted / Solid = actual
+Battery = blue / Load = pink / Solar = orange / Grid = green
+
+
+
Energy Dashboard Forecast
diff --git a/images/raw-data-entity.png b/images/raw-data-entity.png
new file mode 100644
index 0000000000000000000000000000000000000000..e0f7d413dd1c4182d346d1a4a164343b7901e6df
GIT binary patch
literal 71278
zcmd43bx>8|_dW_D7?jeDiZl|^t%7t*NK1EjV<4pF`2Q`8FJ$w+#c}J+b4<*N(4jj2&I{?2V92tZl4}o;VoT8yQ(UnA$jQ
zqtpw*hZqna60tYZb2PKDexPh-WrU>cWb}ZQ>w&0|!Gou)PoF+u<>h1L;^X9eATFz%
z>2@Cn3F!fnr08>H*MyBpH?7FUtLxq2Zq}R|@4i*6XFPa!gCs7_;;XvUat3)!QBjFE
zIlp;qSHFRgalFyr4laJ@qL)t!IbzPtgN(P(DW12WO3U7VE~nyFS<~GyF`izu{!UFHhad2t5
zRZFNizBd>#1%raerG>a?1W_;l}XI3wSe>~PtMggY`xF#)+LB9)_w>RsLU
z<@XjM{PNjwbqtqkN*^aa@wXy;{PbEE_rz4wnb%T=xn;vlU!#KEMtdjDjD9>-?cx_1
z+IEl2Mc%1vd_bPnkDuQyH|?W(bBvbSJDSO^9GcYs*71gS=u&HYlcL`76mot{xmiJh
zJf&eBM}hAnr!?VxzKKs324ig>lD^QtQCxJsyHp_2&F#L7Dz5(2icd08N8eG6rQx%C
zZt9ADp4YrVC4OwpDvu+F;$cH{Qgd&V^w-l?#U!HR2M>qx^r~COL`7eQP5#cEco7xM
zN|Lc|-H=MgWpu>azl=j#rS)%49y9!#jr)~#7mcX_St}2$iH#@BZ{E9ybn~V{y_r|H
zjhxBVF}Z7A{US;DOP!leL-K|Cqo>wt%=|S8w~Ll`=XncaKOdN_i3;c6xGLrh-QGvq
zBdYoCs~Bc{Lw@lo-)Q^#eKhY1We*G5HROeH^+dz@Y7Tx{%-=;naYH$u;vZEMjF0XH
zf9_M-NGK5zl*dJh=j$I!yCEMaL7(NB@W6E=8yFN4zYA4zWM%x;o{I`z
z(AARA?w+2wobuwn!J=}p)@vuFTA4vJ!Bu7T>DQQ(SGUW@mCJwrer4~xeBR_ZZ>Bh3
zHS8Sh`%Qb0YFf=FQqJ?aHOP;Smxf)iD
z)tGM-u+gm*D(YFj4NvxNT+}~#Y*h8@ib?|E)pY^@;P}5v8^r-g{H}Feha7T
zMcUhK=W~=O?<|E*=S)@AqD_UWw$qpPJqMi^LVq-Nt%-p8Gg$)D~u8(D%70=T&W6
zOD2#0(i6R=U&=dK@`lL|4=e~3zS8l=w+Ee~zb5s$kv)nnU2TeX#Tky|R7oFk66s08
z{dC`xihoyp@2dw-vh%E*fOC9UfJkLc*mCp6Gh7N9+sJCxugsHJzKK1z>;pTvcVA26
zayp;~@J6RCSolhe6ADeJ&jf8w3Vn*
zCFU272LPz#G%BHS%>H+r!f@lm{evv8NNRL_Nv@AOGhOViS+(3t0phW2`*wU4$o0&6
ze|_d{L){YsBif!8tV%|E5{bOPvNc9Y_NsYT^7E2NP>%!qqY*A)jE1Mn-^bxH=(Y#jt>A#Q=gG*}N84(CbB7>T=E`l+$5+u
zC%!Y3O2l@`OM`WHcH_EdZgPlFd8R$UVDk!T%T50gF6ABno_%#3HJ&UXr^)5+QS+Z&
z*B&96xD?a^CQm4PwWZx6^(xfehOip=*<%-Ee2XnFEeWK1KRmVVc90?ai@JZr>xmN|
zq$#2K&K_q(ougNXHjF&b_(hZSQFFW8>xLY+^WAETn|-nrT@BwjRBMijHQC>8h%Q~9
zvxl#Vv%Bw!icM(7U+xR(x8x&@T3)VEsj5>E^Vu=rQf|NF-C`F~SLb9b(84bJB=FIu
zUC*}PBI5JmsZR-a-fFW*>8HVcRy&s`R20jb7ph1^m^{}y7LAo9Rq-(vbf*#?DEQ0Bm5ty$!1(PM+VR(dj6TJL<3bz;{=
z$IAiDVEc&1drCZqM|C8cxcGzi&N99zrtI7EnDN|e`||7k1|g7
z@qS23N{>82y5QOmhFulakVVp2JphTC$+
z>~E;7do*%#DNUZ8H{R;?Jx>Xo=JM}3zoj5-g&xREPPAK&^}736BC;suM2Ei{;YKfY
z?8^aWrp!$GB$Zkya^>go6>C=xF8-JrLzoSs0
zUDgR-n^M$p2NJ4X$|wzczYaR$ejId<`(6=3-Xuc4_C6RBOrquw
z53pW&DVBw~J)`jM#>ImNK@YL3i`nIcn;sd<Pr1iHYE5OlDEdcIHFJT+w`U)mNfH5cOIdD03W|zE1H`^(e39Xv>}?3msSJ@
zTvvsI#>Mv|!^F*{o(|ebqqLoOQ=j+5C9uX{nI7*I6&m%+2#q;J4(@Z&73l3_H>%p6
zR>+gdencZjkHD+nx3K)RZHXh!PA{YUHNn>9lZQ*|Bu3dZE^Rcw`+XAbWcpXmI0Fok
ze{;tN(rSknT|61PiuGHxTq%46I|CC$^^-^?KN+Bsa^;^TJh7$XU$ZgV&gLjQZ+
zA`<-YX53qJpEA_U1c|r4U(S5AtXt1SKNDd-n~P2z@s?g#>}R}km9N7bOF(5Rsm*>u
zx+G_^H*4D{qJeXtSTbgf`0qgAZ}fd^u^l#~EgG>>F8dQlWd2?}
zR^f-C)TIeuM@Agy4RSxpOgVaH6nxpFd^)l>t!|0R?7~tfAbex3BEpV=So3F8>Z&Tv
zHGfjrwUgx?O>dN^PlM(vcj~+>Ci$~1Q&LRV6frLm1SwwUEv9B&ESk&bH?IC9EpZ98
z6B62%KO;98rMW%Z-^e=))xcXsri+arBdib76%7
zMBWZU94h$6YAA<$`;siI3V+x{K9Os_MP_%N3te{To@}1e-@b(&U}Ku1
z9mRZCL5G>naD67krJv=u(N>F5nA*;HZ4nadY5#8j{R!>k#x{Es0du5uDYyM6j@vl{
zG4p;Mfx_6R2+mn)Ms%5>jjkl0)Y3GKT|ccmHB`H6k6iCdvNQfrj>jIgY;HQ~TKzfB
zIo7i`=ghBb?h3zM9xLi1liNH)wq3X_&eAUXgF;BkycB6o*ZHj562~B>fU861atG^x
zgyUTy_J=Q3&QAi>zN}ZIjfCJ-bKTjFzW@5u8?4(6ebF-JQ>d8RdDZ$?5tFTK9ZpGEV}r+t=PTFny!gp=_sFB
z;c#C2!H?xWNXGd{7ZdCfg7c2QwhJj?x@;K_=`a6Qxu~v+qSC1&G!-|kiwk=KPxIA$
zrXgX){i!sRtz9s*qL+s|3dA;lMt_ZCQhU<+
z8F?_z%;>rb|1p`|BfCpaq0_6|T=BU(daWxTs~LY>hPU^>GLb%r+A*Ju+NmjJIx%My
zmrn~%D0tUbFQ4LFp6AD|q!o^vFe24`;bzGbDYKUOcy2yaZ$I<-=Yb%Usc=ifbm|Cq
zhS=0Qqye#02j9qX)x*xr@|GH#jhE8w_Q>984xdguR@mV$BGH)MQpvL|T>Y}ksXBWC
zmb%J!scAHEHn|=qQRHWK-_iZw;^7YtvLeK6X6IUY=p$1P49dFL)`n&A-w#b6q*AaI
z&s6t)t?T|+`O^=}hgd>ydtTxrhO|-d#i@)gw)Ydhdjs8xxP-DKH}4((ekRwg{mb7b
zr=eC+(|vL>tR|PM$jUdE(yF@DsHvv!YZ5_EGn;l$eXRzMND|45ZZ+wzyqu}F?gRYd
zzw5=HWqL67$|zjl$I#+Q#;2_5Hc@5wj=qg1L?C;~aE-6Q+tD|(d@VC;(xBCYE!{;Y
zCP1RXWAqTo+tJPOW|1l
zwB*Ab(Xq-^Lt2_P9B~ZUQ3%Pn397c+it;)>Kvb{DNjLX{l*X|#<84;j7+!6P>i6{8s61alm#=XtQrGkTzSq7
z<`u^Ho{#GD>T|+zYS+e@4c|mXt+gf5Y1#J;S#(JIXMbitw5y#|u8a}JMgg*wITL^|
zAgq16b3HknXr!I-oM+`Vj)@s@Y6C4_)u&M50y^q*xBL!8$HzoL&QLElMioQY6!!0x
z(P_`NsG6DLTY@`OyE&Oeg;?LM(8Mt-iHc?=#hb7A|5xZMTX7Hl#zw#_9GPSP8=u6w
zN(@k1YlFXfWQ#uq2Lw!HVW6SN$;t8X^3KlAK7aNMNl#DD(8R>y
z;>6B*b4;cGVJBvas9!*Ua=_Y9HZBE4NN8xNy9`3HzpSjR?EfK&%S3>_y1Lrj(qgyN
z9UFutokvB@AS$}>@o9XIunt!fpFseM@&*xM>-{}e|KC@(QEoxVm+yGNQb!8N4O*6
zT}M#7FXA#vTNjs?QUAOZ(mXo#?|KhZ;{S7gtlTF5F=8Ofx8MJIc7C+U$cIshCZ!w@
zAW^y2pXzU}1pg=DXK!zh7!yAEM=ff^B>b58;OA%NjEI9wJOM#Ln3C)j)&+)t-qm8G
zC6@s>Kzta87`YYP@@~{J5B~QqJ;I$^OfhH+$&yCYl&GLzZ@$4h#AP3*ze<5j3(4d^h3B
zVNd^<#AH23*o2klrRGCfEB6)%?9UF&u1@=W1}Ys)Uc7kmmCH)wrT^nck5oPhRV!m6
zCb(T?|KK18@!7EC |