core/homeassistant/components/ps4/media_player.py

373 lines
11 KiB
Python

"""
Support for PlayStation 4 consoles.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.ps4/
"""
from datetime import timedelta
import logging
import socket
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
from homeassistant.components.media_player import (
MediaPlayerDevice, ENTITY_IMAGE_URL)
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE,
SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
)
from homeassistant.components.ps4.const import DOMAIN as PS4_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_COMMAND, CONF_HOST, CONF_NAME, CONF_REGION,
CONF_TOKEN, STATE_IDLE, STATE_OFF, STATE_PLAYING,
)
from homeassistant.util.json import load_json, save_json
DEPENDENCIES = ['ps4']
_LOGGER = logging.getLogger(__name__)
SUPPORT_PS4 = SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \
SUPPORT_STOP | SUPPORT_SELECT_SOURCE
PS4_DATA = 'ps4_data'
ICON = 'mdi:playstation'
GAMES_FILE = '.ps4-games.json'
MEDIA_IMAGE_DEFAULT = None
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=10)
COMMANDS = (
'up',
'down',
'right',
'left',
'enter',
'back',
'option',
'ps',
)
SERVICE_COMMAND = 'send_command'
PS4_COMMAND_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_COMMAND): vol.In(list(COMMANDS))
})
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up PS4 from a config entry."""
config = config_entry
def add_entities(entities, update_before_add=False):
"""Sync version of async add devices."""
hass.add_job(async_add_entities, entities, update_before_add)
await hass.async_add_executor_job(
setup_platform, hass, config,
add_entities, None)
async def async_service_handle(hass):
"""Handle for services."""
def service_command(call):
entity_ids = call.data[ATTR_ENTITY_ID]
command = call.data[ATTR_COMMAND]
for device in hass.data[PS4_DATA].devices:
if device.entity_id in entity_ids:
device.send_command(command)
hass.services.async_register(
PS4_DOMAIN, SERVICE_COMMAND, service_command,
schema=PS4_COMMAND_SCHEMA)
await async_service_handle(hass)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up PS4 Platform."""
import pyps4_homeassistant as pyps4
hass.data[PS4_DATA] = PS4Data()
games_file = hass.config.path(GAMES_FILE)
creds = config.data[CONF_TOKEN]
device_list = []
for device in config.data['devices']:
host = device[CONF_HOST]
region = device[CONF_REGION]
name = device[CONF_NAME]
ps4 = pyps4.Ps4(host, creds)
device_list.append(PS4Device(
name, host, region, ps4, games_file))
add_entities(device_list, True)
class PS4Data():
"""Init Data Class."""
def __init__(self):
"""Init Class."""
self.devices = []
class PS4Device(MediaPlayerDevice):
"""Representation of a PS4."""
def __init__(self, name, host, region, ps4, games_file):
"""Initialize the ps4 device."""
self._ps4 = ps4
self._host = host
self._name = name
self._region = region
self._state = None
self._games_filename = games_file
self._media_content_id = None
self._media_title = None
self._media_image = None
self._source = None
self._games = {}
self._source_list = []
self._retry = 0
self._info = None
self._unique_id = None
async def async_added_to_hass(self):
"""Subscribe PS4 events."""
self.hass.data[PS4_DATA].devices.append(self)
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Retrieve the latest data."""
try:
status = self._ps4.get_status()
if self._info is None:
self.get_device_info(status)
self._games = self.load_games()
if self._games is not None:
self._source_list = list(sorted(self._games.values()))
except socket.timeout:
status = None
if status is not None:
self._retry = 0
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
self.get_title_data(title_id, name)
else:
self.idle()
else:
self.state_off()
elif self._retry > 5:
self.state_unknown()
else:
self._retry += 1
def idle(self):
"""Set states for state idle."""
self.reset_title()
self._state = STATE_IDLE
def state_off(self):
"""Set states for state off."""
self.reset_title()
self._state = STATE_OFF
def state_unknown(self):
"""Set states for state unknown."""
self.reset_title()
self._state = None
_LOGGER.warning("PS4 could not be reached")
self._retry = 0
def reset_title(self):
"""Update if there is no title."""
self._media_title = None
self._media_content_id = None
self._source = None
def get_title_data(self, title_id, name):
"""Get PS Store Data."""
app_name = None
art = None
try:
app_name, art = self._ps4.get_ps_store_data(
name, title_id, self._region)
except TypeError:
_LOGGER.error(
"Could not find data in region: %s for PS ID: %s",
self._region, title_id)
finally:
self._media_title = app_name or name
self._source = self._media_title
self._media_image = art
self.update_list()
def update_list(self):
"""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:
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._games = self.load_games()
self._source_list = list(sorted(self._games.values()))
def load_games(self):
"""Load games for sources."""
g_file = self._games_filename
try:
games = load_json(g_file)
# If file does not exist, create empty file.
except FileNotFoundError:
games = {}
self.save_games(games)
return games
def save_games(self, games):
"""Save games to file."""
g_file = self._games_filename
try:
save_json(g_file, games)
except OSError as error:
_LOGGER.error("Could not save game list, %s", error)
# Retry loading file
if games is None:
self.load_games()
def add_games(self, title_id, app_name):
"""Add games to list."""
games = self._games
if title_id is not None and title_id not in games:
game = {title_id: app_name}
games.update(game)
self.save_games(games)
def get_device_info(self, status):
"""Return device info for registry."""
_sw_version = status['system-version']
_sw_version = _sw_version[1:4]
sw_version = "{}.{}".format(_sw_version[0], _sw_version[1:])
self._info = {
'name': status['host-name'],
'model': 'PlayStation 4',
'identifiers': {
(PS4_DOMAIN, status['host-id'])
},
'manufacturer': 'Sony Interactive Entertainment Inc.',
'sw_version': sw_version
}
self._unique_id = status['host-id']
@property
def device_info(self):
"""Return information about the device."""
return self._info
@property
def unique_id(self):
"""Return Unique ID for entity."""
return self._unique_id
@property
def entity_picture(self):
"""Return picture."""
if self._state == STATE_PLAYING and self._media_content_id is not None:
image_hash = self.media_image_hash
if image_hash is not None:
return ENTITY_IMAGE_URL.format(
self.entity_id, self.access_token, image_hash)
return MEDIA_IMAGE_DEFAULT
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def icon(self):
"""Icon."""
return ICON
@property
def media_content_id(self):
"""Content ID of current playing media."""
return self._media_content_id
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def media_image_url(self):
"""Image url of current playing media."""
if self._media_content_id is None:
return MEDIA_IMAGE_DEFAULT
return self._media_image
@property
def media_title(self):
"""Title of current playing media."""
return self._media_title
@property
def supported_features(self):
"""Media player features that are supported."""
return SUPPORT_PS4
@property
def source(self):
"""Return the current input source."""
return self._source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
def turn_off(self):
"""Turn off media player."""
self._ps4.standby()
def turn_on(self):
"""Turn on the media player."""
self._ps4.wakeup()
def media_pause(self):
"""Send keypress ps to return to menu."""
self._ps4.remote_control('ps')
def media_stop(self):
"""Send keypress ps to return to menu."""
self._ps4.remote_control('ps')
def select_source(self, source):
"""Select input source."""
for title_id, game in self._games.items():
if source == game:
_LOGGER.debug(
"Starting PS4 game %s (%s) using source %s",
game, title_id, source)
self._ps4.start_title(
title_id, running_id=self._media_content_id)
return
def send_command(self, command):
"""Send Button Command."""
self._ps4.remote_control(command)