Skip to content

Commit

Permalink
Merge pull request #508 from alandtse/offline_detect
Browse files Browse the repository at this point in the history
feat: add offline device detection
  • Loading branch information
alandtse committed Jan 15, 2020
2 parents 7ed8fbe + 3bd8045 commit 1ee9556
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 103 deletions.
159 changes: 87 additions & 72 deletions custom_components/alexa_media/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
CONF_INCLUDE_DEVICES,
DATA_ALEXAMEDIA,
DOMAIN,
ISSUE_URL,
MIN_TIME_BETWEEN_FORCED_SCANS,
MIN_TIME_BETWEEN_SCANS,
SCAN_INTERVAL,
Expand Down Expand Up @@ -72,9 +73,9 @@
{
vol.Optional(CONF_ACCOUNTS): vol.All(
cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA]
),
)
}
),
)
},
extra=vol.ALLOW_EXTRA,
)
Expand All @@ -94,6 +95,7 @@


async def async_setup(hass, config, discovery_info=None):
# pylint: disable=unused-argument
"""Set up the Alexa domain."""
if DOMAIN not in config:
return True
Expand Down Expand Up @@ -148,9 +150,7 @@ async def close_alexa_media(event=None) -> None:
for email, _ in hass.data[DATA_ALEXAMEDIA]["accounts"].items():
await close_connections(hass, email)

if DATA_ALEXAMEDIA not in hass.data:
hass.data[DATA_ALEXAMEDIA] = {}
hass.data[DATA_ALEXAMEDIA]["accounts"] = {}
hass.data.setdefault(DATA_ALEXAMEDIA, {"accounts": {}})
from alexapy import AlexaLogin, __version__ as alexapy_version

_LOGGER.info(STARTUP)
Expand All @@ -160,22 +160,29 @@ async def close_alexa_media(event=None) -> None:
email = account.get(CONF_EMAIL)
password = account.get(CONF_PASSWORD)
url = account.get(CONF_URL)
if email not in hass.data[DATA_ALEXAMEDIA]["accounts"]:
hass.data[DATA_ALEXAMEDIA]["accounts"][email] = {}
if "login_obj" in hass.data[DATA_ALEXAMEDIA]["accounts"][email]:
login = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"]
else:
login = AlexaLogin(
url, email, password, hass.config.path, account.get(CONF_DEBUG)
)
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"]) = login
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["config_entry"]) = config_entry
(
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["setup_platform_callback"]
) = setup_platform_callback
(
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["test_login_status"]
) = test_login_status
hass.data[DATA_ALEXAMEDIA]["accounts"].setdefault(
email,
{
"config_entry": config_entry,
"setup_platform_callback": setup_platform_callback,
"test_login_status": test_login_status,
"devices": {"media_player": {}},
"entities": {"media_player": {}},
"excluded": {},
"new_devices": True,
"websocket_lastattempt": 0,
"websocketerror": 0,
"websocket_commands": {},
"websocket_activity": {"serials": {}, "refreshed": {}},
"websocket": None,
"auth_info": None,
"configurator": [],
},
)
login = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
"login_obj",
AlexaLogin(url, email, password, hass.config.path, account.get(CONF_DEBUG)),
)
await login.login_with_cookie()
await test_login_status(hass, config_entry, login, setup_platform_callback)
return True
Expand Down Expand Up @@ -324,8 +331,6 @@ async def configuration_callback(callback_data):
submit_caption="Confirm",
fields=[],
)
if "configurator" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]:
hass.data[DATA_ALEXAMEDIA]["accounts"][email] = {"configurator": []}
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"].append(config_id)
if "error_message" in status and status["error_message"]:
configurator.async_notify_errors(config_id, status["error_message"])
Expand Down Expand Up @@ -404,8 +409,7 @@ async def update_devices(login_obj):
This will add new devices and services when discovered. By default this
runs every SCAN_INTERVAL seconds unless another method calls it. if
websockets is connected, it will return immediately unless
'new_devices' has been set to True.
websockets is connected, it will increase the delay 10-fold between updates.
While throttled at MIN_TIME_BETWEEN_SCANS, care should be taken to
reduce the number of runs to avoid flooding. Slow changing states
should be checked here instead of in spawned components like
Expand All @@ -421,15 +425,19 @@ async def update_devices(login_obj):
existing_entities = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"][
"media_player"
].values()
if (
"websocket" in hass.data[DATA_ALEXAMEDIA]["accounts"][email]
and hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]
and not (hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"])
):
return
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False
websocket_enabled = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get(
"websocket"
)
auth_info = hass.data[DATA_ALEXAMEDIA]["accounts"][email].get("auth_info")
new_devices = hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"]
devices = {}
bluetooth = {}
preferences = {}
dnd = {}
raw_notifications = {}
try:
auth_info = await AlexaAPI.get_authentication(login_obj)
if new_devices:
auth_info = await AlexaAPI.get_authentication(login_obj)
devices = await AlexaAPI.get_devices(login_obj)
bluetooth = await AlexaAPI.get_bluetooth(login_obj)
preferences = await AlexaAPI.get_device_preferences(login_obj)
Expand All @@ -439,7 +447,9 @@ async def update_devices(login_obj):
"%s: Found %s devices, %s bluetooth",
hide_email(email),
len(devices) if devices is not None else "",
len(bluetooth) if bluetooth is not None else "",
len(bluetooth.get("bluetoothStates", []))
if bluetooth is not None
else "",
)
if (devices is None or bluetooth is None) and not (
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["configurator"]
Expand All @@ -454,6 +464,9 @@ async def update_devices(login_obj):
hass, config_entry, login_obj, setup_platform_callback
)
return
await process_notifications(login_obj, raw_notifications)
# Process last_called data to fire events
await update_last_called(login_obj)

new_alexa_clients = [] # list of newly discovered device names
exclude_filter = []
Expand Down Expand Up @@ -495,6 +508,7 @@ async def update_devices(login_obj):
for b_state in bluetooth["bluetoothStates"]:
if device["serialNumber"] == b_state["deviceSerialNumber"]:
device["bluetooth_state"] = b_state
break

if "devicePreferences" in preferences:
for dev in preferences["devicePreferences"]:
Expand All @@ -507,6 +521,7 @@ async def update_devices(login_obj):
device["timeZoneId"],
hide_serial(device["serialNumber"]),
)
break

if "doNotDisturbDeviceStatusList" in dnd:
for dev in dnd["doNotDisturbDeviceStatusList"]:
Expand All @@ -517,7 +532,10 @@ async def update_devices(login_obj):
device["dnd"],
hide_serial(device["serialNumber"]),
)
device["auth_info"] = auth_info
break
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["auth_info"] = device[
"auth_info"
] = auth_info
(
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"][
"media_player"
Expand Down Expand Up @@ -559,14 +577,14 @@ async def update_devices(login_obj):
)
)

await process_notifications(login_obj, raw_notifications)
# Process last_called data to fire events
await update_last_called(login_obj)
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"] = False
async_call_later(
hass,
scan_interval,
scan_interval if not websocket_enabled else scan_interval * 10,
lambda _: hass.async_create_task(
update_devices(login_obj, no_throttle=True)
update_devices( # pylint: disable=unexpected-keyword-arg
login_obj, no_throttle=True
)
),
)

Expand Down Expand Up @@ -660,12 +678,12 @@ async def update_bluetooth_state(login_obj, device_serial):
async def clear_history(call):
"""Handle clear history service request.
Arguments:
Arguments
call.ATTR_EMAIL {List[str: None]} -- Case-sensitive Alexa emails.
Default is all known emails.
call.ATTR_NUM_ENTRIES {int: 50} -- Number of entries to delete.
Returns:
Returns
bool -- True if deletion successful
"""
Expand Down Expand Up @@ -715,7 +733,7 @@ async def ws_connect() -> WebsocketEchoClient:
)
_LOGGER.debug("%s: Websocket created: %s", hide_email(email), websocket)
await websocket.async_run()
except BaseException as exception_:
except BaseException as exception_: # pylint: disable=broad-except
_LOGGER.debug(
"%s: Websocket creation failed: %s", hide_email(email), exception_
)
Expand All @@ -728,6 +746,8 @@ async def ws_handler(message_obj):
This allows push notifications from Alexa to update last_called
and media state.
"""
import time

command = (
message_obj.json_payload["command"]
if isinstance(message_obj.json_payload, dict)
Expand All @@ -741,13 +761,10 @@ async def ws_handler(message_obj):
else None
)
existing_serials = _existing_serials()
if "websocket_commands" not in (hass.data[DATA_ALEXAMEDIA]["accounts"][email]):
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_commands"]) = {}
seen_commands = hass.data[DATA_ALEXAMEDIA]["accounts"][email][
"websocket_commands"
]
if command and json_payload:
import time

_LOGGER.debug(
"%s: Received websocket command: %s : %s",
Expand Down Expand Up @@ -866,6 +883,17 @@ async def ws_handler(message_obj):
f"{DOMAIN}_{hide_email(email)}"[0:32],
{"notification_update": json_payload},
)
else:
_LOGGER.warning(
"Unhandled command: %s with data %s. Please report at %s",
command,
hide_serial(json_payload),
ISSUE_URL,
)
if serial in existing_serials:
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_activity"][
"serials"
][serial] = time.time()
if (
serial
and serial not in existing_serials
Expand All @@ -876,7 +904,9 @@ async def ws_handler(message_obj):
):
_LOGGER.debug("Discovered new media_player %s", serial)
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"]) = True
await update_devices(login_obj, no_throttle=True)
await update_devices( # pylint: disable=unexpected-keyword-arg
login_obj, no_throttle=True
)

async def ws_open_handler():
"""Handle websocket open."""
Expand Down Expand Up @@ -926,12 +956,13 @@ async def ws_close_handler():
) = await ws_connect()
errors += 1
delay = 5 * 2 ** errors
else:
_LOGGER.debug(
"%s: Websocket closed; retries exceeded; polling", hide_email(email)
)
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]) = None
await update_devices(login_obj, no_throttle=True)
await update_devices( # pylint: disable=unexpected-keyword-arg
login_obj, no_throttle=True
)

async def ws_error_handler(message):
"""Handle websocket error.
Expand All @@ -957,27 +988,11 @@ async def ws_error_handler(message):
if isinstance(config.get(CONF_SCAN_INTERVAL), timedelta)
else config.get(CONF_SCAN_INTERVAL)
)
if "login_obj" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]:
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"]) = login_obj
if "devices" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]:
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["devices"]) = {
"media_player": {}
}
if "excluded" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]:
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["excluded"]) = {}
if "entities" not in hass.data[DATA_ALEXAMEDIA]["accounts"][email]:
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["entities"]) = {
"media_player": {}
}
(
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["new_devices"]
) = True # force initial update
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket_lastattempt"]) = 0
(
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocketerror"]
) = 0 # set errors to 0
(hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]) = await ws_connect()
await update_devices(login_obj, no_throttle=True)
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["login_obj"] = login_obj
hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] = await ws_connect()
await update_devices( # pylint: disable=unexpected-keyword-arg
login_obj, no_throttle=True
)
hass.services.async_register(
DOMAIN,
SERVICE_UPDATE_LAST_CALLED,
Expand All @@ -999,9 +1014,9 @@ async def async_unload_entry(hass, entry) -> bool:
for component in ALEXA_COMPONENTS:
await hass.config_entries.async_forward_entry_unload(entry, component)
# notify has to be handled manually as the forward does not work yet
from .notify import async_unload_entry
from .notify import notify_async_unload_entry

await async_unload_entry(hass, entry)
await notify_async_unload_entry(hass, entry)
email = entry.data["email"]
await close_connections(hass, email)
await clear_configurator(hass, email)
Expand Down
1 change: 1 addition & 0 deletions custom_components/alexa_media/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ async def async_added_to_hass(self):
self._listener = self.hass.bus.async_listen(
f"{ALEXA_DOMAIN}_{hide_email(self._login.email)}"[0:32], self._handle_event
)
await self.async_update()

async def async_will_remove_from_hass(self):
"""Prepare to remove entity."""
Expand Down
2 changes: 1 addition & 1 deletion custom_components/alexa_media/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""
from datetime import timedelta

__version__ = '2.4.1'
__version__ = "2.4.1"
PROJECT_URL = "https://github.com/custom-components/alexa_media_player/"
ISSUE_URL = "{}issues".format(PROJECT_URL)

Expand Down
Loading

0 comments on commit 1ee9556

Please sign in to comment.