From 48540fc21ec195b34b1385e5c11b50be0bb7a6da Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Fri, 19 Jul 2019 22:36:45 -0700 Subject: [PATCH] Ps4 reformat media data (#25172) * Reformat saved media data/ fix load + save helpers * Add url constant * Reformat saved media data * Add tests for media data * Refactor * Revert deleted lines * Set attrs after checking for lock * Patch load games. * remove unneeded imports * fix tests * Correct condition * Handle errors with loading games * Correct condition * Fix select source * add test * Remove unneeded vars * line break * cleanup loading json * remove test * move check for dict * Set games to {} --- homeassistant/components/ps4/__init__.py | 47 ++++++-- homeassistant/components/ps4/const.py | 1 + homeassistant/components/ps4/media_player.py | 70 +++++++++--- tests/components/ps4/test_init.py | 106 ++++++++++++++++++- 4 files changed, 200 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 31cdc4310f2..48e05ba6105 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -1,20 +1,25 @@ """Support for PlayStation 4 consoles.""" import logging +import os import voluptuous as vol from pyps4_homeassistant.ddp import async_create_ddp_endpoint from pyps4_homeassistant.media_art import COUNTRIES +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, MEDIA_TYPE_GAME) from homeassistant.const import ( - ATTR_COMMAND, ATTR_ENTITY_ID, CONF_REGION, CONF_TOKEN) + ATTR_COMMAND, ATTR_ENTITY_ID, ATTR_LOCKED, CONF_REGION, CONF_TOKEN) from homeassistant.core import split_entity_id +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry, config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json from .config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import -from .const import COMMANDS, DOMAIN, GAMES_FILE, PS4_DATA +from .const import ( + ATTR_MEDIA_IMAGE_URL, COMMANDS, DOMAIN, GAMES_FILE, PS4_DATA) _LOGGER = logging.getLogger(__name__) @@ -141,12 +146,22 @@ def load_games(hass: HomeAssistantType) -> dict: """Load games for sources.""" g_file = hass.config.path(GAMES_FILE) try: - games = load_json(g_file) + games = load_json(g_file, dict) + except HomeAssistantError as error: + games = {} + _LOGGER.error("Failed to load games file: %s", error) + + if not isinstance(games, dict): + _LOGGER.error("Games file was not parsed correctly") + games = {} # If file does not exist, create empty file. - except FileNotFoundError: + if not os.path.isfile(g_file): + _LOGGER.info("Creating PS4 Games File") games = {} save_games(hass, games) + else: + games = _reformat_data(hass, games) return games @@ -158,9 +173,27 @@ def save_games(hass: HomeAssistantType, games: dict): except OSError as error: _LOGGER.error("Could not save game list, %s", error) - # Retry loading file - if games is None: - load_games(hass) + +def _reformat_data(hass: HomeAssistantType, games: dict) -> dict: + """Reformat data to correct format.""" + data_reformatted = False + + for game, data in games.items(): + # Convert str format to dict format. + if not isinstance(data, dict): + # Use existing title. Assign defaults. + games[game] = {ATTR_LOCKED: False, + ATTR_MEDIA_TITLE: data, + ATTR_MEDIA_IMAGE_URL: None, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME} + data_reformatted = True + + _LOGGER.debug( + "Reformatting media data for item: %s, %s", game, data) + + if data_reformatted: + save_games(hass, games) + return games def service_handle(hass: HomeAssistantType): diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index e2e1e943552..43b23c26666 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -1,4 +1,5 @@ """Constants for PlayStation 4.""" +ATTR_MEDIA_IMAGE_URL = 'media_image_url' CONFIG_ENTRY_VERSION = 3 DEFAULT_NAME = "PlayStation 4" DEFAULT_REGION = "United States" diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 2430d7b4d3f..dfa8fe79e0b 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -9,17 +9,19 @@ from homeassistant.core import callback from homeassistant.components.media_player import ( ENTITY_IMAGE_URL, MediaPlayerDevice) from homeassistant.components.media_player.const import ( - MEDIA_TYPE_GAME, MEDIA_TYPE_APP, SUPPORT_SELECT_SOURCE, - SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) + ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, + MEDIA_TYPE_GAME, MEDIA_TYPE_APP, SUPPORT_SELECT_SOURCE, SUPPORT_PAUSE, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.components.ps4 import ( format_unique_id, load_games, save_games) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_REGION, - CONF_TOKEN, STATE_IDLE, STATE_OFF, STATE_PLAYING) + ATTR_LOCKED, CONF_HOST, CONF_NAME, CONF_REGION, CONF_TOKEN, + STATE_IDLE, STATE_OFF, STATE_PLAYING) from homeassistant.helpers import device_registry, entity_registry -from .const import (DEFAULT_ALIAS, DOMAIN as PS4_DOMAIN, PS4_DATA, - REGIONS as deprecated_regions) +from .const import ( + ATTR_MEDIA_IMAGE_URL, DEFAULT_ALIAS, DOMAIN as PS4_DOMAIN, + PS4_DATA, REGIONS as deprecated_regions) _LOGGER = logging.getLogger(__name__) @@ -147,20 +149,29 @@ class PS4Device(MediaPlayerDevice): if status is not None: self._games = load_games(self.hass) - if self._games is not None: - self._source_list = list(sorted(self._games.values())) + if self._games: + self.get_source_list() + self._retry = 0 self._disconnected = False if status.get('status') == 'Ok': title_id = status.get('running-app-titleid') name = status.get('running-app-name') + if title_id and name is not None: self._state = STATE_PLAYING + if self._media_content_id != title_id: self._media_content_id = title_id + if self._use_saved(): + _LOGGER.debug( + "Using saved data for media: %s", title_id) + return + self._media_title = name self._source = self._media_title self._media_type = None + # Get data from PS Store. asyncio.ensure_future( self.async_get_title_data(title_id, name)) else: @@ -175,6 +186,23 @@ class PS4Device(MediaPlayerDevice): else: self._retry += 1 + def _use_saved(self) -> bool: + """Return True, Set media attrs if data is locked.""" + if self._media_content_id in self._games: + store = self._games[self._media_content_id] + + # If locked get attributes from file. + locked = store.get(ATTR_LOCKED) + if locked: + self._media_title = store.get(ATTR_MEDIA_TITLE) + self._source = self._media_title + self._media_image = store.get( + ATTR_MEDIA_IMAGE_URL) + self._media_type = store.get( + ATTR_MEDIA_CONTENT_TYPE) + return True + return False + def idle(self): """Set states for state idle.""" self.reset_title() @@ -246,20 +274,33 @@ class PS4Device(MediaPlayerDevice): """Update Game List, Correct data if different.""" if self._media_content_id in self._games: store = self._games[self._media_content_id] - if store != self._media_title: + + if store.get(ATTR_MEDIA_TITLE) != self._media_title or\ + store.get(ATTR_MEDIA_IMAGE_URL) != self._media_image: self._games.pop(self._media_content_id) if self._media_content_id not in self._games: - self.add_games(self._media_content_id, self._media_title) + self.add_games( + self._media_content_id, self._media_title, + self._media_image, self._media_type) self._games = load_games(self.hass) - self._source_list = list(sorted(self._games.values())) + self.get_source_list() - def add_games(self, title_id, app_name): + def get_source_list(self): + """Parse data entry and update source list.""" + games = [] + for data in self._games.values(): + games.append(data[ATTR_MEDIA_TITLE]) + self._source_list = sorted(games) + + def add_games(self, title_id, app_name, image, g_type, is_locked=False): """Add games to list.""" games = self._games if title_id is not None and title_id not in games: - game = {title_id: app_name} + game = {title_id: { + ATTR_MEDIA_TITLE: app_name, ATTR_MEDIA_IMAGE_URL: image, + ATTR_MEDIA_CONTENT_TYPE: g_type, ATTR_LOCKED: is_locked}} games.update(game) save_games(self.hass, games) @@ -399,7 +440,8 @@ class PS4Device(MediaPlayerDevice): async def async_select_source(self, source): """Select input source.""" - for title_id, game in self._games.items(): + for title_id, data in self._games.items(): + game = data[ATTR_MEDIA_TITLE] if source.lower().encode(encoding='utf-8') == \ game.lower().encode(encoding='utf-8') \ or source == title_id: diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index c9f56f7b334..00b7d83aa97 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -1,14 +1,17 @@ """Tests for the PS4 Integration.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import ps4 +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, MEDIA_TYPE_GAME) from homeassistant.components.ps4.const import ( - COMMANDS, CONFIG_ENTRY_VERSION as VERSION, + ATTR_MEDIA_IMAGE_URL, COMMANDS, CONFIG_ENTRY_VERSION as VERSION, DEFAULT_REGION, DOMAIN, PS4_DATA) from homeassistant.const import ( - ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST, + ATTR_COMMAND, ATTR_ENTITY_ID, ATTR_LOCKED, CONF_HOST, CONF_NAME, CONF_REGION, CONF_TOKEN) +from homeassistant.exceptions import HomeAssistantError from homeassistant.util import location from homeassistant.setup import async_setup_component from tests.common import (MockConfigEntry, mock_coro, mock_registry) @@ -63,6 +66,29 @@ MOCK_ENTRY_VERSION_1 = MockConfigEntry( MOCK_UNIQUE_ID = 'someuniqueid' +MOCK_ID = 'CUSA00123' +MOCK_URL = 'http://someurl.jpeg' +MOCK_TITLE = 'Some Title' +MOCK_TYPE = MEDIA_TYPE_GAME + +MOCK_GAMES_DATA_OLD_STR_FORMAT = {'mock_id': 'mock_title', + 'mock_id2': 'mock_title2'} + +MOCK_GAMES_DATA = { + ATTR_LOCKED: False, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_IMAGE_URL: MOCK_URL, + ATTR_MEDIA_TITLE: MOCK_TITLE} + +MOCK_GAMES_DATA_LOCKED = { + ATTR_LOCKED: True, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_IMAGE_URL: MOCK_URL, + ATTR_MEDIA_TITLE: MOCK_TITLE} + +MOCK_GAMES = {MOCK_ID: MOCK_GAMES_DATA} +MOCK_GAMES_LOCKED = {MOCK_ID: MOCK_GAMES_DATA_LOCKED} + async def test_ps4_integration_setup(hass): """Test PS4 integration is setup.""" @@ -147,6 +173,80 @@ async def setup_mock_component(hass): await hass.async_block_till_done() +def test_games_reformat_to_dict(hass): + """Test old data format is converted to new format.""" + with patch('homeassistant.components.ps4.load_json', + return_value=MOCK_GAMES_DATA_OLD_STR_FORMAT),\ + patch('homeassistant.components.ps4.save_json', + side_effect=MagicMock()),\ + patch('os.path.isfile', return_value=True): + mock_games = ps4.load_games(hass) + + # New format is a nested dict. + assert isinstance(mock_games, dict) + assert mock_games['mock_id'][ATTR_MEDIA_TITLE] == 'mock_title' + assert mock_games['mock_id2'][ATTR_MEDIA_TITLE] == 'mock_title2' + for mock_game in mock_games: + mock_data = mock_games[mock_game] + assert isinstance(mock_data, dict) + assert mock_data + assert mock_data[ATTR_MEDIA_IMAGE_URL] is None + assert mock_data[ATTR_LOCKED] is False + assert mock_data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_GAME + + +def test_load_games(hass): + """Test that games are loaded correctly.""" + with patch('homeassistant.components.ps4.load_json', + return_value=MOCK_GAMES),\ + patch('homeassistant.components.ps4.save_json', + side_effect=MagicMock()),\ + patch('os.path.isfile', return_value=True): + mock_games = ps4.load_games(hass) + + assert isinstance(mock_games, dict) + + mock_data = mock_games[MOCK_ID] + assert isinstance(mock_data, dict) + assert mock_data[ATTR_MEDIA_TITLE] == MOCK_TITLE + assert mock_data[ATTR_MEDIA_IMAGE_URL] == MOCK_URL + assert mock_data[ATTR_LOCKED] is False + assert mock_data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_GAME + + +def test_loading_games_returns_dict(hass): + """Test that loading games always returns a dict.""" + with patch('homeassistant.components.ps4.load_json', + side_effect=HomeAssistantError),\ + patch('homeassistant.components.ps4.save_json', + side_effect=MagicMock()),\ + patch('os.path.isfile', return_value=True): + mock_games = ps4.load_games(hass) + + assert isinstance(mock_games, dict) + assert not mock_games + + with patch('homeassistant.components.ps4.load_json', + return_value='Some String'),\ + patch('homeassistant.components.ps4.save_json', + side_effect=MagicMock()),\ + patch('os.path.isfile', return_value=True): + mock_games = ps4.load_games(hass) + + assert isinstance(mock_games, dict) + assert not mock_games + + with patch('homeassistant.components.ps4.load_json', + return_value=[]),\ + patch('homeassistant.components.ps4.save_json', + side_effect=MagicMock()),\ + patch('os.path.isfile', return_value=True): + mock_games = ps4.load_games(hass) + + assert isinstance(mock_games, dict) + assert not mock_games + + async def test_send_command(hass): """Test send_command service.""" await setup_mock_component(hass)