diff --git a/custom_components/alexa_media/__init__.py b/custom_components/alexa_media/__init__.py index 7ef77d00..185adc05 100644 --- a/custom_components/alexa_media/__init__.py +++ b/custom_components/alexa_media/__init__.py @@ -38,6 +38,7 @@ CONF_INCLUDE_DEVICES, DATA_ALEXAMEDIA, DOMAIN, + ISSUE_URL, MIN_TIME_BETWEEN_FORCED_SCANS, MIN_TIME_BETWEEN_SCANS, SCAN_INTERVAL, @@ -72,9 +73,9 @@ { vol.Optional(CONF_ACCOUNTS): vol.All( cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA] - ), + ) } - ), + ) }, extra=vol.ALLOW_EXTRA, ) @@ -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 @@ -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) @@ -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 @@ -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"]) @@ -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 @@ -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) @@ -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"] @@ -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 = [] @@ -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"]: @@ -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"]: @@ -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" @@ -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 + ) ), ) @@ -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 """ @@ -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_ ) @@ -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) @@ -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", @@ -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 @@ -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.""" @@ -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. @@ -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, @@ -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) diff --git a/custom_components/alexa_media/alarm_control_panel.py b/custom_components/alexa_media/alarm_control_panel.py index 76fefa20..4f4ddd41 100644 --- a/custom_components/alexa_media/alarm_control_panel.py +++ b/custom_components/alexa_media/alarm_control_panel.py @@ -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.""" diff --git a/custom_components/alexa_media/const.py b/custom_components/alexa_media/const.py index 4cc8d79c..674f5534 100644 --- a/custom_components/alexa_media/const.py +++ b/custom_components/alexa_media/const.py @@ -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) diff --git a/custom_components/alexa_media/media_player.py b/custom_components/alexa_media/media_player.py index b4de82b0..842c3a68 100644 --- a/custom_components/alexa_media/media_player.py +++ b/custom_components/alexa_media/media_player.py @@ -28,7 +28,13 @@ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY +from homeassistant.const import ( + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, + STATE_UNAVAILABLE, +) from homeassistant.helpers.event import async_call_later from . import ( @@ -63,8 +69,9 @@ DEPENDENCIES = [ALEXA_DOMAIN] -@retry_async(limit=5, delay=2, catch_exceptions=True) +@retry_async(limit=5, delay=2, catch_exceptions=False) async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): + # pylint: disable=unused-argument """Set up the Alexa media player platform.""" devices = [] # type: List[AlexaClient] account = config[CONF_EMAIL] @@ -109,6 +116,7 @@ class AlexaClient(MediaPlayerDevice): """Representation of a Alexa device.""" def __init__(self, device, login): + # pylint: disable=unused-argument """Initialize the Alexa device.""" from alexapy import AlexaAPI @@ -157,11 +165,17 @@ def __init__(self, device, login): self._playing_parent = None # Last Device self._last_called = None + self._last_called_timestamp = None # Do not Disturb state self._dnd = None # Polling state self._should_poll = True - self._last_update = 0 + self._last_update = util.utcnow() + self._listener = None + self._bluetooth_state = None + self._app_device_list = None + self._parent_clusters = None + self._timezone = None async def init(self, device): """Initialize.""" @@ -209,14 +223,17 @@ async def _refresh_if_no_audiopush(already_refreshed=False): if ( not already_refreshed and seen_commands - and ( - "PUSH_AUDIO_PLAYER_STATE" not in seen_commands - and "PUSH_MEDIA_CHANGE" not in seen_commands + and not ( + "PUSH_AUDIO_PLAYER_STATE" in seen_commands + or "PUSH_MEDIA_CHANGE" in seen_commands + or "PUSH_MEDIA_PROGRESS_CHANGE" in seen_commands ) ): # force refresh if player_state update not found, see #397 _LOGGER.debug( - "%s: No PUSH_AUDIO_PLAYER_STATE in %s; forcing refresh", + "%s: No PUSH_AUDIO_PLAYER_STATE/" + "PUSH_MEDIA_CHANGE/PUSH_MEDIA_PROGRESS_CHANGE in %s;" + "forcing refresh", hide_email(email), seen_commands, ) @@ -253,9 +270,9 @@ async def _refresh_if_no_audiopush(already_refreshed=False): if event.data["queue_state"] else None ) - if not event_serial: + if not event_serial or event_serial != self.device_serial_number: return - self._available = True + self.available = True self.async_schedule_update_ha_state() if "last_called_change" in event.data: if event_serial == self.device_serial_number or any( @@ -267,6 +284,9 @@ async def _refresh_if_no_audiopush(already_refreshed=False): hide_serial(self.device_serial_number), ) self._last_called = True + self._last_called_timestamp = event.data["last_called_change"][ + "timestamp" + ] else: self._last_called = False if self.hass and self.async_schedule_update_ha_state: @@ -300,9 +320,10 @@ async def _refresh_if_no_audiopush(already_refreshed=False): self.name, player_state["audioPlayerState"], ) + # allow delay before trying to refresh to avoid http 400 errors + await asyncio.sleep(0.5) await self.async_update() already_refreshed = True - # refresh is necessary to pull all data elif "mediaReferenceId" in player_state: _LOGGER.debug( "%s media update: %s", @@ -311,7 +332,6 @@ async def _refresh_if_no_audiopush(already_refreshed=False): ) await self.async_update() already_refreshed = True - # refresh is necessary to pull all data elif "volumeSetting" in player_state: _LOGGER.debug( "%s volume updated: %s", @@ -322,7 +342,7 @@ async def _refresh_if_no_audiopush(already_refreshed=False): if self.hass and self.async_schedule_update_ha_state: self.async_schedule_update_ha_state() elif "dopplerConnectionState" in player_state: - self._available = player_state["dopplerConnectionState"] == "ONLINE" + self.available = player_state["dopplerConnectionState"] == "ONLINE" if self.hass and self.async_schedule_update_ha_state: self.async_schedule_update_ha_state() await _refresh_if_no_audiopush(already_refreshed) @@ -407,7 +427,7 @@ async def refresh(self, device=None): self._dnd = device["dnd"] if "dnd" in device else None await self._set_authentication_details(device["auth_info"]) session = None - if self._available: + if self.available: _LOGGER.debug("%s: Refreshing %s", self.account, self.name) if self._parent_clusters and self.hass: playing_parents = list( @@ -430,6 +450,10 @@ async def refresh(self, device=None): self._source = await self._get_source() self._source_list = await self._get_source_list() self._last_called = await self._get_last_called() + if self._last_called: + self._last_called_timestamp = self.hass.data[DATA_ALEXAMEDIA][ + "accounts" + ][self._login.email]["last_called"]["timestamp"] if "MUSIC_SKILL" in self._capabilities: parent_session = {} if playing_parents: @@ -450,7 +474,8 @@ async def refresh(self, device=None): parent_session["lemurVolume"]["memberVolume"][ self.device_serial_number ] - if parent_session.get("lemurVolume", {}) + if parent_session.get("lemurVolume") + and parent_session.get("lemurVolume", {}) .get("memberVolume", {}) .get(self.device_serial_number) else session["volume"] @@ -495,10 +520,15 @@ async def refresh(self, device=None): "mediaLength" ) if not self._session.get("lemurVolume"): - self._media_is_muted = self._session.get("volume", {}).get("muted") + self._media_is_muted = ( + self._session.get("volume", {}).get("muted") + if self._session.get("volume") + else self._media_is_muted + ) self._media_vol_level = ( self._session["volume"]["volume"] / 100 - if self._session.get("volume", {}).get("volume") + if self._session.get("volume") + and self._session.get("volume", {}).get("volume") else self._media_vol_level ) else: @@ -615,6 +645,11 @@ def available(self): """Return the availability of the client.""" return self._available + @available.setter + def available(self, state): + """Set the availability state.""" + self._available = self._device["online"] = state + @property def unique_id(self): """Return the id of this Alexa client.""" @@ -643,6 +678,8 @@ def session(self): @property def state(self): """Return the state of the device.""" + if not self.available: + return STATE_UNAVAILABLE if self._media_player_state == "PLAYING": return STATE_PLAYING if self._media_player_state == "PAUSED": @@ -683,19 +720,24 @@ async def async_update(self): in (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]) else None ) - await self.refresh( - device, no_throttle=True # pylint: disable=unexpected-keyword-arg + await self.refresh( # pylint: disable=unexpected-keyword-arg + device, no_throttle=True + ) + websocket_enabled = self.hass.data[DATA_ALEXAMEDIA]["accounts"][email].get( + "websocket" ) if ( self.state in [STATE_PLAYING] and # only enable polling if websocket not connected ( - not self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"] - or - # or if no PUSH_AUDIO_PLAYER_STATE - not seen_commands - or "PUSH_AUDIO_PLAYER_STATE" not in seen_commands + not websocket_enabled + or not seen_commands + or not ( + "PUSH_AUDIO_PLAYER_STATE" in seen_commands + or "PUSH_MEDIA_CHANGE" in seen_commands + or "PUSH_MEDIA_PROGRESS_CHANGE" in seen_commands + ) ) ): self._should_poll = False # disable polling since manual update @@ -717,7 +759,7 @@ async def async_update(self): ) elif self._should_poll: # Not playing, one last poll self._should_poll = False - if not (self.hass.data[DATA_ALEXAMEDIA]["accounts"][email]["websocket"]): + if not websocket_enabled: _LOGGER.debug( "Disabling polling and scheduling last update in" " 300 seconds for %s", @@ -799,7 +841,7 @@ def dnd_state(self, state): async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" await self.alexa_api.shuffle(shuffle) - self.shuffle_state = shuffle + self._shuffle = shuffle @property def shuffle(self): @@ -810,6 +852,7 @@ def shuffle(self): def shuffle(self, state): """Set the Shuffle state.""" self._shuffle = state + self.async_schedule_update_ha_state() @property def repeat_state(self): @@ -820,6 +863,7 @@ def repeat_state(self): def repeat_state(self, state): """Set the Repeat state.""" self._repeat = state + self.async_schedule_update_ha_state() @property def supported_features(self): @@ -858,7 +902,7 @@ async def async_mute_volume(self, mute): - On mute, store volume and set volume to 0 - On unmute, set volume to previously stored volume """ - if not (self.state == STATE_PLAYING and self.available): + if not self.available: return self._media_is_muted = mute @@ -976,6 +1020,7 @@ async def async_send_mobilepush(self, message, **kwargs): @_catch_login_errors async def async_play_media(self, media_type, media_id, enqueue=None, **kwargs): + # pylint: disable=unused-argument """Send the play_media command to the media player.""" if media_type == "music": await self.async_send_tts( @@ -983,6 +1028,12 @@ async def async_play_media(self, media_type, media_id, enqueue=None, **kwargs): " with the notify.alexa_media service." " Please see the alexa_media wiki for details." ) + _LOGGER.warning( + "Sorry, text to speech can only be called" + " with the notify.alexa_media service." + " Please see the alexa_media wiki for details." + "https://github.com/custom-components/alexa_media_player/wiki/Notification-Component#use-the-notifyalexa_media-service" + ) elif media_type == "sequence": await self.alexa_api.send_sequence( media_id, customer_id=self._customer_id, **kwargs @@ -1004,8 +1055,12 @@ async def async_play_media(self, media_type, media_id, enqueue=None, **kwargs): @property def device_state_attributes(self): - """Return the scene state attributes.""" - attr = {"available": self._available, "last_called": self._last_called} + """Return the state attributes.""" + attr = { + "available": self.available, + "last_called": self._last_called, + "last_called_timestamp": self._last_called_timestamp, + } return attr @property diff --git a/custom_components/alexa_media/sensor.py b/custom_components/alexa_media/sensor.py index 897397ef..ecbaa18e 100644 --- a/custom_components/alexa_media/sensor.py +++ b/custom_components/alexa_media/sensor.py @@ -178,7 +178,11 @@ def __init__( def _fix_alarm_date_time(self, value): import pytz - if self._sensor_property != "date_time" or not value: + if ( + self._sensor_property != "date_time" + or not value + or isinstance(value[1][self._sensor_property], datetime.datetime) + ): return value naive_time = dt.parse_datetime(value[1][self._sensor_property]) timezone = pytz.timezone(self._client._timezone) diff --git a/setup.cfg b/setup.cfg index 5feccdc1..1c79378a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,8 @@ max-line-length = 88 # D202 No blank lines allowed after function docstring # W504 line break after binary operator # H102 missing apache +# H306 Alphabetically order your imports by the full module path. + ignore = E501, H301, @@ -23,7 +25,8 @@ ignore = E203, D202, W504, - H102 + H102, + H306 [isort] # https://github.com/timothycrosley/isort