Skip to content

Commit

Permalink
Improve update event handling efficiency
Browse files Browse the repository at this point in the history
  • Loading branch information
ekutner committed Oct 22, 2023
1 parent c123048 commit 3dd985c
Showing 1 changed file with 90 additions and 60 deletions.
150 changes: 90 additions & 60 deletions home_connect_async/appliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class Option():
max:Optional[int] = None
stepsize:Optional[int] = None
allowedvalues:Optional[list[str]] = None
displayallowedvalues:Optional[list[str]] = None
allowedvaluesdisplay:Optional[list[str]] = None
execution:Optional[str] = None
liveupdate:Optional[bool] = None
default:Optional[str] = None
Expand All @@ -92,7 +92,7 @@ def create(cls, data:dict):
option.max = constraints.get('max')
option.stepsize = constraints.get('stepsize')
option.allowedvalues = constraints.get('allowedvalues')
option.displayallowedvalues = constraints.get('displayvalues')
option.allowedvaluesdisplay = constraints.get('displayvalues')
option.execution = constraints.get('execution')
option.liveupdate = constraints.get('liveupdate')
option.default = constraints.get('default')
Expand Down Expand Up @@ -485,46 +485,9 @@ async def async_update_data(self, data:dict) -> None:
await self.async_fetch_data()
await self._callbacks.async_broadcast_event(self, Events.PAIRED)
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)
else:
# update options, statuses and settings in the data model
if self.selected_program and self.selected_program.options and key in self.selected_program.options:
self.selected_program.options[key].value = value
self.selected_program.options[key].name = data.get("name")
self.selected_program.options[key].displayvalue = data.get("displayvalue")
elif "programs/selected" in uri:
_LOGGER.debug("Got event for unknown property: %s", data)
self.active_program = await self._async_fetch_programs("selected")
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

if self.active_program and self.active_program.options and key in self.active_program.options:
self.active_program.options[key].value = value
self.active_program.options[key].name = data.get("name")
self.active_program.options[key].displayvalue = data.get("displayvalue")
elif "programs/active" in uri:
_LOGGER.debug("Got event for unknown property: %s", data)
self.active_program = await self._async_fetch_programs("active")
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

if key in self.status:
self.status[key].value = value
self.status[key].name = data.get("name")
self.status[key].displayvalue = data.get("displayvalue")
elif "/status/" in uri:
_LOGGER.debug("Got event for unknown property: %s", data)
self.status = await self._async_fetch_status()
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

if key in self.settings:
self.settings[key].value = value
self.settings[key].name = data.get("name")
self.settings[key].displayvalue = data.get("displayvalue")
elif "/settings/" in uri:
_LOGGER.debug("Got event for unknown property: %s", data)
self.settings = await self._async_fetch_settings()
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

# Fetch data from the API on major events
if key == "BSH.Common.Root.SelectedProgram" and (not self.selected_program or self.selected_program.key != value):
elif key == "BSH.Common.Root.SelectedProgram" and (not self.selected_program or self.selected_program.key != value):
# handle selected program
async with Synchronization.selected_program_lock:
# Have to check again after aquiring the lock
Expand All @@ -551,7 +514,7 @@ async def async_update_data(self, data:dict) -> None:
# apparently it is possible to get progress notifications without getting the ActiveProgram event first so we handle that
(key in ["BSH.Common.Option.ProgramProgress", "BSH.Common.Option.RemainingProgramTime"]) or
# it is also possible to get operation state Run without getting the ActiveProgram event
(key == "BSH.Common.Status.OperationState" and value=="BSH.Common.EnumType.OperationState.Run")
(key == "BSH.Common.Status.OperationState" and value in ["BSH.Common.EnumType.OperationState.Run", "BSH.Common.EnumType.OperationState.DelayedStart"])
) and \
(not self.active_program or (key == "BSH.Common.Root.ActiveProgram" and self.active_program.key != value) ) and \
self._active_program_fail_count < 3 :
Expand All @@ -560,53 +523,120 @@ async def async_update_data(self, data:dict) -> None:
if self.active_program:
self._active_program_fail_count = 0
else:
# This is a workaround to prevent rate limiting when receiving progress events but avaialable_programs returns 404
# This is a workaround to prevent rate limiting when receiving progress events but active_program returns 404
self._active_program_fail_count += 1
self.available_programs = await self._async_fetch_programs("available")
self.settings = await self._async_fetch_settings()
if not self.settings:
self.settings = await self._async_fetch_settings()
self.commands = await self._async_fetch_commands()
await self._callbacks.async_broadcast_event(self, Events.PROGRAM_STARTED, value)
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

elif ( (key == "BSH.Common.Root.ActiveProgram" and not value) or
(key == "BSH.Common.Status.OperationState" and value in ["BSH.Common.EnumType.OperationState.Ready", "BSH.Common.EnumType.OperationState.Finished"]) or
(key == "BSH.Common.Status.OperationState" and value == "BSH.Common.EnumType.OperationState.Finished") or
(key == "BSH.Common.Event.ProgramFinished")
) and self.active_program:
# handle program end

# NOTE: Depending on the received event there may still be an active program provided by the API
# This creates an inconsistency of HA is restarted while the appliance is in this state
# however, it still seems bettert than relying on the order of received events which is very inconsistent

prev_prog = self.active_program.key if self.active_program else None
self.active_program = None
self._active_program_fail_count = 0
self.settings = await self._async_fetch_settings()
self.commands = await self._async_fetch_commands()
self.available_programs = await self._async_fetch_programs("available")
# TODO: should self.available_programs = None ????
await self._callbacks.async_broadcast_event(self, Events.PROGRAM_FINISHED, prev_prog)
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

elif key == "BSH.Common.Status.OperationState" and \
value!="BSH.Common.EnumType.OperationState.Run" and \
self.status.get("BSH.Common.Status.OperationState") != value: # ignore repeat notifiations of the same state
self.active_program = await self._async_fetch_programs("active")
elif key == "BSH.Common.Status.OperationState" and value == "BSH.Common.EnumType.OperationState.Ready" \
and self.status.get("BSH.Common.Status.OperationState", {"value": None}).value != value: # ignore repeating events
prev_prog = self.active_program.key if self.active_program else None
self.active_program = None
self._active_program_fail_count = 0
self.selected_program = await self._async_fetch_programs("selected")
self.available_programs = await self._async_fetch_programs("available")
self.settings = await self._async_fetch_settings()
self.commands = await self._async_fetch_commands()
if not self.settings:
self.settings = await self._async_fetch_settings()
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)
if prev_prog:
await self._callbacks.async_broadcast_event(self, Events.PROGRAM_FINISHED, prev_prog)

elif key == "BSH.Common.Status.OperationState" and value == "BSH.Common.EnumType.OperationState.Inactive" \
and self.status.get("BSH.Common.Status.OperationState", {"value": None}).value != value:
self.active_program = None
self._active_program_fail_count = 0
self.selected_program = None
self.available_programs = None
self.commands = {}
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

elif key == "BSH.Common.Status.OperationState" and value == "BSH.Common.EnumType.OperationState.Pause" \
and self.status.get("BSH.Common.Status.OperationState", {"value": None}).value != value:
self.commands = await self._async_fetch_commands()

elif key == "BSH.Common.Status.OperationState" \
and value in [ "BSH.Common.EnumType.OperationState.ActionRequired", "BSH.Common.EnumType.OperationState.Error", "BSH.Common.EnumType.OperationState.Aborting" ] \
and self.status.get("BSH.Common.Status.OperationState", {"value": None}).value != value:
_LOGGER.debug("The appliance entered and error operation state: %s", data)

elif key =="BSH.Common.Status.RemoteControlStartAllowed":
self.available_programs = await self._async_fetch_programs("available")
self.commands = await self._async_fetch_commands()
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

elif ( not self.available_programs or len(self.available_programs) < 2) and \
( key in ["BSH.Common.Status.OperationState", "BSH.Common.Status.RemoteControlActive"] ) and \
( "BSH.Common.Status.OperationState" not in self.status or self.status["BSH.Common.Status.OperationState"].value == "BSH.Common.EnumType.OperationState.Ready" ) and \
( "BSH.Common.Status.RemoteControlActive" not in self.status or self.status["BSH.Common.Status.RemoteControlActive"].value):
# Handle cases were the appliance data was loaded without getting all the programs (for example when HA is restarted while a program is active)
# If the state is Ready and remote control is possible and we didn't load the available programs before then load them now
available_programs = await self._async_fetch_programs("available")
self.available_programs = available_programs
await self._callbacks.async_broadcast_event(self, Events.PAIRED)
# elif ( not self.available_programs or len(self.available_programs) < 2) and \
# ( key in ["BSH.Common.Status.OperationState", "BSH.Common.Status.RemoteControlActive"] ) and \
# ( "BSH.Common.Status.OperationState" not in self.status or self.status["BSH.Common.Status.OperationState"].value == "BSH.Common.EnumType.OperationState.Ready" ) and \
# ( "BSH.Common.Status.RemoteControlActive" not in self.status or self.status["BSH.Common.Status.RemoteControlActive"].value):
# # Handle cases were the appliance data was loaded without getting all the programs (for example when HA is restarted while a program is active)
# # If the state is Ready and remote control is possible and we didn't load the available programs before then load them now
# available_programs = await self._async_fetch_programs("available")
# self.available_programs = available_programs
# await self._callbacks.async_broadcast_event(self, Events.PAIRED)
# await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

if self.selected_program and self.selected_program.options and key in self.selected_program.options:
self.selected_program.options[key].value = value
self.selected_program.options[key].name = data.get("name")
self.selected_program.options[key].displayvalue = data.get("displayvalue")
elif "programs/selected" in uri and key != "BSH.Common.Root.SelectedProgram":
_LOGGER.debug("Got event for unknown property: %s", data)
self.active_program = await self._async_fetch_programs("selected")
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

if self.active_program and self.active_program.options and key in self.active_program.options:
self.active_program.options[key].value = value
self.active_program.options[key].name = data.get("name")
self.active_program.options[key].displayvalue = data.get("displayvalue")
elif ( "programs/active" in uri and key != "BSH.Common.Root.ActiveProgram"
# ignore late active program events coming after the program has finished
# this implies that an unknown event will not triger the first active program fetch, which should be fine
and self.active_program
):
_LOGGER.debug("Got event for unknown property: %s", data)
self.active_program = await self._async_fetch_programs("active")
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

if key in self.status:
self.status[key].value = value
self.status[key].name = data.get("name")
self.status[key].displayvalue = data.get("displayvalue")
elif "/status/" in uri:
_LOGGER.debug("Got event for unknown property: %s", data)
self.status = await self._async_fetch_status()
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)

if key in self.settings:
self.settings[key].value = value
self.settings[key].name = data.get("name")
self.settings[key].displayvalue = data.get("displayvalue")
elif "/settings/" in uri:
_LOGGER.debug("Got event for unknown property: %s", data)
self.settings = await self._async_fetch_settings()
await self._callbacks.async_broadcast_event(self, Events.DATA_CHANGED)
# broadcast the specific event that was received
await self._callbacks.async_broadcast_event(self, key, value)

Expand Down

0 comments on commit 3dd985c

Please sign in to comment.