Skip to content

Commit

Permalink
Set Fox to charge by default to mitigate cloud outages
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanmarlor committed Jan 25, 2023
1 parent 6367426 commit e734a57
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 49 deletions.
23 changes: 10 additions & 13 deletions custom_components/foxess_em/charge/charge_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,15 @@ def _add_listeners(self) -> None:
async def _eco_start_setup(self, *args) -> None: # pylint: disable=unused-argument
"""Set target SoC"""

_LOGGER.debug("Resetting any existing Fox Cloud force charge/min SoC settings")
await self._fox.stop_force_charge()
await self._fox.set_min_soc(self._original_soc * 100)

_LOGGER.debug("Calculating optimal battery SoC")
await self._forecast_controller.async_refresh()
self._charge_required = self._battery_controller.charge_total()
self._perc_target = self._battery_controller.charge_to_perc()

_LOGGER.debug("Resetting any existing Fox Cloud force charge/min SoC settings")
await self._start_force_charge()
await self._fox.set_min_soc(self._original_soc * 100)

async def _eco_start(self, *args) -> None: # pylint: disable=unused-argument
"""Eco start"""

Expand All @@ -107,12 +107,11 @@ async def _eco_start(self, *args) -> None: # pylint: disable=unused-argument

self._start_listening()

if self._charge_required > 0:
await self._start_force_charge()
else:
if self._charge_required <= 0:
_LOGGER.debug(
f"Allowing battery to continue discharge until {self._perc_target}"
)
await self._stop_force_charge()

_LOGGER.debug("Resetting switches")
self._battery_controller.set_boost(False)
Expand All @@ -121,16 +120,14 @@ async def _eco_start(self, *args) -> None: # pylint: disable=unused-argument
async def _start_force_charge(
self, *args
) -> None: # pylint: disable=unused-argument
"""Battery SoC has not yet met desired percentage"""
_LOGGER.debug(f"Starting force charge to {self._perc_target}")
"""Set Fox force charge settings to True"""
self._charge_active = True
await self._fox.start_force_charge_off_peak()

async def _stop_force_charge(
self, *args
) -> None: # pylint: disable=unused-argument
"""Battery SoC has met desired percentage"""
_LOGGER.debug("Stopping force charge")
"""Set Fox force charge settings to False"""
self._charge_active = False
await self._fox.stop_force_charge()

Expand All @@ -139,8 +136,8 @@ async def _eco_end(self, *args) -> None: # pylint: disable=unused-argument

self._stop_listening()

if self._charge_active:
await self._stop_force_charge()
# Reset Fox force charge to enabled
await self._start_force_charge()

_LOGGER.debug("Releasing SoC hold")
await self._fox.set_min_soc(self._original_soc * 100)
Expand Down
2 changes: 2 additions & 0 deletions custom_components/foxess_em/fox/fox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ async def _post_data(self, url: str, params: dict[str, str]) -> dict:
response = await self._session.post(
url, json=params, headers=self._token
)
# Leave 1 second between subsequent Fox calls
await asyncio.sleep(1)
except Exception as ex:
raise NoDataError(f"Fox Cloud API error: {ex}")

Expand Down
120 changes: 84 additions & 36 deletions custom_components/foxess_em/fox/fox_cloud_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
from datetime import time

from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_utc_time_change
from homeassistant.helpers.event import async_call_later

from ..common.unload_controller import UnloadController
from ..fox.fox_api import FoxApiClient
from ..util.exceptions import NoDataError

Expand All @@ -18,7 +17,7 @@
_LOGGER = logging.getLogger(__name__)


class FoxCloudService(UnloadController):
class FoxCloudService:
"""Fox Cloud service"""

def __init__(
Expand All @@ -30,61 +29,62 @@ def __init__(
user_min_soc: int = 11,
) -> None:
"""Init Fox Cloud service"""
UnloadController.__init__(self)
self._hass = hass
self._api = api
self._off_peak_start = off_peak_start
self._off_peak_end = off_peak_end
self._user_min_soc = user_min_soc
self._device_sn = None
self._off_peak_listener = None

if hass is not None:
async_call_later(hass, 5, self.start_force_charge_off_peak)

async def start_force_charge_now(self, *args) -> None:
"""Start force charge now"""
now = datetime.now().astimezone()
start = now.replace(hour=0, minute=1).time()
stop = now.replace(hour=23, minute=59).time()

await self._start_force_charge(start, stop)
device_sn = await self.device_serial_number()
query = self._build_single_charge_query(device_sn, True, start, stop)

await self._start_force_charge(query)

async def start_force_charge_off_peak(self, *args) -> None:
"""Start force charge off peak"""
device_sn = await self.device_serial_number()
if self._off_peak_start > self._off_peak_end:
_LOGGER.debug("Setting charge to midnight first")
# Off-peak period crosses midnight
before_midnight = time(hour=23, minute=59)
await self._start_force_charge(self._off_peak_start, before_midnight)
# Setup trigger to reset times after midnight
midnight = async_track_utc_time_change(
self._hass,
self._finish_force_charge_off_peak,
hour=before_midnight.hour,
minute=before_midnight.minute,
second=0,
local=True,
after_midnight = time(hour=0, minute=1)

query = self._build_double_charge_query(
device_sn,
True,
self._off_peak_start,
before_midnight,
after_midnight,
self._off_peak_end,
)
self._off_peak_listener = midnight
self._unload_listeners.append(midnight)
else:
await self._start_force_charge(self._off_peak_start, self._off_peak_end)

async def _finish_force_charge_off_peak(self, *args) -> None:
"""Finish force charge off peak"""
_LOGGER.debug("Finishing charge from midnight to eco end")
self._off_peak_listener()
self._unload_listeners.remove(self._off_peak_listener)
query = self._build_single_charge_query(
device_sn,
True,
self._off_peak_start,
self._off_peak_end,
)

after_midnight = time(hour=0, minute=1)
await self._start_force_charge(after_midnight, self._off_peak_end)
await self._start_force_charge(query)

async def _start_force_charge(self, start, stop) -> None:
async def _start_force_charge(self, query: dict) -> None:
"""Start force charge"""
_LOGGER.debug("Requesting start force charge from Fox Cloud")

try:
device_sn = await self.device_serial_number()
await self._api.async_post_data(
f"{_BASE_URL}{_SET_TIMES}",
self._build_charge_start_stop_query(device_sn, True, start, stop),
query,
)
except NoDataError as ex:
_LOGGER.error(ex)
Expand All @@ -95,14 +95,17 @@ async def stop_force_charge(self, *args) -> None: # pylint: disable=unused-argu

try:
device_sn = await self.device_serial_number()

query = self._build_single_charge_query(
device_sn,
False,
self._off_peak_start,
self._off_peak_end,
)

await self._api.async_post_data(
f"{_BASE_URL}{_SET_TIMES}",
self._build_charge_start_stop_query(
device_sn,
False,
self._off_peak_start,
self._off_peak_end,
),
query,
)
except NoDataError as ex:
_LOGGER.error(ex)
Expand Down Expand Up @@ -145,7 +148,7 @@ def _build_min_soc_query(self, device_sn: str, soc: int) -> dict:
"""Build min SoC query object"""
return {"sn": device_sn, "minGridSoc": soc, "minSoc": self._user_min_soc * 100}

def _build_charge_start_stop_query(
def _build_single_charge_query(
self, device_sn: str, start_stop: bool, start_time: time, end_time: time
) -> dict:
"""Build device query object"""
Expand Down Expand Up @@ -177,3 +180,48 @@ def _build_charge_start_stop_query(
}

return query

def _build_double_charge_query(
self,
device_sn: str,
start_stop: bool,
first_start_time: time,
first_end_time: time,
second_start_time: time,
second_end_time: time,
) -> dict:
"""Build device query object"""

query = {
"sn": device_sn,
"times": [
{
"tip": "",
"enableCharge": start_stop,
"enableGrid": start_stop,
"startTime": {
"hour": str(first_start_time.hour).zfill(2),
"minute": str(first_start_time.minute).zfill(2),
},
"endTime": {
"hour": str(first_end_time.hour).zfill(2),
"minute": str(first_end_time.minute).zfill(2),
},
},
{
"tip": "",
"enableCharge": start_stop,
"enableGrid": start_stop,
"startTime": {
"hour": str(second_start_time.hour).zfill(2),
"minute": str(second_start_time.minute).zfill(2),
},
"endTime": {
"hour": str(second_end_time.hour).zfill(2),
"minute": str(second_end_time.minute).zfill(2),
},
},
],
}

return query

0 comments on commit e734a57

Please sign in to comment.