Skip to content

Commit

Permalink
Merge pull request #2578 from alandtse/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
alandtse authored Oct 3, 2024
2 parents 889e4af + 0cfe145 commit 4e2d226
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 70 deletions.
104 changes: 75 additions & 29 deletions custom_components/alexa_media/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,11 @@ async def setup_alexa(hass, config_entry, login_obj: AlexaLogin):
# pylint: disable=too-many-statements,too-many-locals
"""Set up a alexa api based on host parameter."""

# Initialize throttling state and lock
last_dnd_update_times: dict[str, datetime] = {}
pending_dnd_updates: dict[str, bool] = {}
dnd_update_lock = asyncio.Lock()

async def async_update_data() -> Optional[AlexaEntityData]:
# noqa pylint: disable=too-many-branches
"""Fetch data from API endpoint.
Expand Down Expand Up @@ -644,31 +649,17 @@ async def async_update_data() -> Optional[AlexaEntityData]:
cleaned_config = config.copy()
cleaned_config.pop(CONF_PASSWORD, None)
# CONF_PASSWORD contains sensitive info which is no longer needed
for component in ALEXA_COMPONENTS:
entry_setup = len(
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][component]
# Load multiple platforms in parallel using async_forward_entry_setups
_LOGGER.debug("Loading platforms: %s", ", ".join(ALEXA_COMPONENTS))
try:
await hass.config_entries.async_forward_entry_setups(
config_entry, ALEXA_COMPONENTS
)
if not entry_setup:
_LOGGER.debug("Loading config entry for %s", component)
try:
await hass.config_entries.async_forward_entry_setups(
config_entry, [component]
)
except (asyncio.TimeoutError, TimeoutException) as ex:
raise ConfigEntryNotReady(
f"Timeout while loading config entry for {component}"
) from ex
else:
_LOGGER.debug("Loading %s", component)
hass.async_create_task(
async_load_platform(
hass,
component,
DOMAIN,
{CONF_NAME: DOMAIN, "config": cleaned_config},
cleaned_config,
)
)
except (asyncio.TimeoutError, TimeoutException) as ex:
_LOGGER.error(f"Error while loading platforms: {ex}")
raise ConfigEntryNotReady(
f"Timeout while loading platforms: {ex}"
) from ex

hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False
# prune stale devices
Expand Down Expand Up @@ -843,21 +834,76 @@ async def update_bluetooth_state(login_obj, device_serial):
)
return None

@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
async def schedule_update_dnd_state(email: str):
"""Schedule an update_dnd_state call after MIN_TIME_BETWEEN_FORCED_SCANS."""
await asyncio.sleep(MIN_TIME_BETWEEN_FORCED_SCANS)
async with dnd_update_lock:
if pending_dnd_updates.get(email, False):
pending_dnd_updates[email] = False
_LOGGER.debug(
"Executing scheduled forced DND update for %s", hide_email(email)
)
# Assume login_obj can be retrieved or passed appropriately
login_obj = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"]
await update_dnd_state(login_obj)

@_catch_login_errors
async def update_dnd_state(login_obj) -> None:
"""Update the dnd state on ws dnd combo event."""
dnd = await AlexaAPI.get_dnd_state(login_obj)
"""Update the DND state on websocket DND combo event."""
email = login_obj.email
now = datetime.utcnow()

async with dnd_update_lock:
last_run = last_dnd_update_times.get(email)
cooldown = timedelta(seconds=MIN_TIME_BETWEEN_SCANS)

if last_run and (now - last_run) < cooldown:
# If within cooldown, mark a pending update if not already marked
if not pending_dnd_updates.get(email, False):
pending_dnd_updates[email] = True
_LOGGER.debug(
"Throttling active for %s, scheduling a forced DND update.",
hide_email(email),
)
asyncio.create_task(schedule_update_dnd_state(email))
else:
_LOGGER.debug(
"Throttling active for %s, forced DND update already scheduled.",
hide_email(email),
)
return

# Update the last run time
last_dnd_update_times[email] = now

_LOGGER.debug("Updating DND state for %s", hide_email(email))

try:
# Fetch the DND state using the Alexa API
dnd = await AlexaAPI.get_dnd_state(login_obj)
except asyncio.TimeoutError:
_LOGGER.error(
"Timeout occurred while fetching DND state for %s", hide_email(email)
)
return
except Exception as e:
_LOGGER.error(
"Unexpected error while fetching DND state for %s: %s",
hide_email(email),
e,
)
return

# Check if DND data is valid and dispatch an update event
if dnd is not None and "doNotDisturbDeviceStatusList" in dnd:
async_dispatcher_send(
hass,
f"{DOMAIN}_{hide_email(email)}"[0:32],
{"dnd_update": dnd["doNotDisturbDeviceStatusList"]},
)
return
_LOGGER.debug("%s: get_dnd_state failed: dnd:%s", hide_email(email), dnd)
return
else:
_LOGGER.debug("%s: get_dnd_state failed: dnd:%s", hide_email(email), dnd)

async def http2_connect() -> HTTP2EchoClient:
"""Open HTTP2 Push connection.
Expand Down
45 changes: 26 additions & 19 deletions custom_components/alexa_media/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,30 +225,37 @@ def report_relogin_required(hass, login, email) -> bool:


def _existing_serials(hass, login_obj) -> list:
"""Retrieve existing serial numbers for a given login object."""
email: str = login_obj.email
existing_serials = (
list(
if (
DATA_ALEXAMEDIA in hass.data
and "accounts" in hass.data[DATA_ALEXAMEDIA]
and email in hass.data[DATA_ALEXAMEDIA]["accounts"]
):
existing_serials = list(
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
"media_player"
].keys()
)
if "entities" in (hass.data[DATA_ALEXAMEDIA]["accounts"][email])
else []
)
for serial in existing_serials:
device = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][
"media_player"
][serial]
if "appDeviceList" in device and device["appDeviceList"]:
apps = list(
map(
lambda x: x["serialNumber"] if "serialNumber" in x else None,
device["appDeviceList"],
)
)
# _LOGGER.debug("Combining %s with %s",
# existing_serials, apps)
existing_serials = existing_serials + apps
device_data = (
hass.data[DATA_ALEXAMEDIA]["accounts"][email]
.get("devices", {})
.get("media_player", {})
)
for serial in existing_serials:
device = device_data.get(serial, {})
if "appDeviceList" in device and device["appDeviceList"]:
apps = [
x["serialNumber"]
for x in device["appDeviceList"]
if "serialNumber" in x
]
existing_serials.extend(apps)
else:
_LOGGER.warning(
"No accounts data found for %s. Skipping serials retrieval.", email
)
existing_serials = []
return existing_serials


Expand Down
59 changes: 37 additions & 22 deletions custom_components/alexa_media/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,8 +776,12 @@ async def async_select_source(self, source):
else:
await self.alexa_api.set_bluetooth(devices["address"])
self._source = source
# Safely access 'http2' setting
if not (
self.hass.data[DATA_ALEXAMEDIA]["accounts"][self._login.email]["http2"]
self.hass.data.get(DATA_ALEXAMEDIA, {})
.get("accounts", {})
.get(self._login.email, {})
.get("http2")
):
await self.async_update()

Expand Down Expand Up @@ -922,35 +926,47 @@ async def async_update(self):
except AttributeError:
pass
email = self._login.email

# Check if DATA_ALEXAMEDIA and 'accounts' exist
accounts_data = self.hass.data.get(DATA_ALEXAMEDIA, {}).get("accounts", {})
if (
self.entity_id is None # Device has not initialized yet
or email not in self.hass.data[DATA_ALEXAMEDIA]["accounts"]
or email not in accounts_data
or self._login.session.closed
):
self._assumed_state = True
self.available = False
return
device = self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][
"media_player"
][self.device_serial_number]

# Safely access the device
device = accounts_data[email]["devices"]["media_player"].get(
self.device_serial_number
)
if not device:
_LOGGER.warning(
"Device serial number %s not found for account %s. Skipping update.",
self.device_serial_number,
hide_email(email),
)
self.available = False
return

# Safely access websocket_commands
seen_commands = (
self.hass.data[DATA_ALEXAMEDIA]["accounts"][email][
"websocket_commands"
].keys()
if "websocket_commands"
in (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email])
accounts_data[email]["websocket_commands"].keys()
if "websocket_commands" in accounts_data[email]
else None
)
await self.refresh( # pylint: disable=unexpected-keyword-arg
device, no_throttle=True
)
push_enabled = (
self.hass.data[DATA_ALEXAMEDIA]["accounts"].get(email, {}).get("http2")
)

await self.refresh(device, no_throttle=True)

# Safely access 'http2' setting
push_enabled = accounts_data[email].get("http2")

if (
self.state in [MediaPlayerState.PLAYING]
and
# only enable polling if websocket not connected
# Only enable polling if websocket not connected
(
not push_enabled
or not seen_commands
Expand All @@ -970,7 +986,7 @@ async def async_update(self):
):
_LOGGER.debug(
"%s: %s playing; scheduling update in %s seconds",
hide_email(self._login.email),
hide_email(email),
self.name,
PLAY_SCAN_INTERVAL,
)
Expand All @@ -983,9 +999,8 @@ async def async_update(self):
self._should_poll = False
if not push_enabled:
_LOGGER.debug(
"%s: Disabling polling and scheduling last update in"
" 300 seconds for %s",
hide_email(self._login.email),
"%s: Disabling polling and scheduling last update in 300 seconds for %s",
hide_email(email),
self.name,
)
async_call_later(
Expand All @@ -996,7 +1011,7 @@ async def async_update(self):
else:
_LOGGER.debug(
"%s: Disabling polling for %s",
hide_email(self._login.email),
hide_email(email),
self.name,
)
self._last_update = util.utcnow()
Expand Down

0 comments on commit 4e2d226

Please sign in to comment.