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 {}
pull/25332/head
ktnrg45 2019-07-19 22:36:45 -07:00 committed by Martin Hjelmare
parent 693fa15924
commit 48540fc21e
4 changed files with 200 additions and 24 deletions

View File

@ -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):

View File

@ -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"

View File

@ -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:

View File

@ -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)