diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index b91e6b239e7..7f7561304d2 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -7,13 +7,29 @@ from homeassistant.helpers import entity_registry from homeassistant.util import location from .config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import -from .const import DOMAIN # noqa: pylint: disable=unused-import +from .const import DOMAIN, PS4_DATA # noqa: pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) +class PS4Data(): + """Init Data Class.""" + + def __init__(self): + """Init Class.""" + self.devices = [] + self.protocol = None + + async def async_setup(hass, config): """Set up the PS4 Component.""" + from pyps4_homeassistant.ddp import async_create_ddp_endpoint + + hass.data[PS4_DATA] = PS4Data() + + transport, protocol = await async_create_ddp_endpoint() + hass.data[PS4_DATA].protocol = protocol + _LOGGER.debug("PS4 DDP endpoint created: %s, %s", transport, protocol) return True diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index b31ba44fbe3..8ef98e12a8f 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -9,7 +9,7 @@ from homeassistant.const import ( CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN) from homeassistant.util import location -from .const import DEFAULT_NAME, DOMAIN +from .const import DEFAULT_ALIAS, DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -61,7 +61,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): if user_input is not None: try: self.creds = await self.hass.async_add_executor_job( - self.helper.get_creds) + self.helper.get_creds, DEFAULT_ALIAS) if self.creds is not None: return await self.async_step_mode() return self.async_abort(reason='credential_error') @@ -143,7 +143,8 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): self.host = user_input[CONF_IP_ADDRESS] is_ready, is_login = await self.hass.async_add_executor_job( - self.helper.link, self.host, self.creds, self.pin) + self.helper.link, self.host, + self.creds, self.pin, DEFAULT_ALIAS) if is_ready is False: errors['base'] = 'not_ready' diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index bbf654530b0..3c0dad6119f 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -1,7 +1,9 @@ """Constants for PlayStation 4.""" DEFAULT_NAME = "PlayStation 4" DEFAULT_REGION = "United States" +DEFAULT_ALIAS = 'Home-Assistant' DOMAIN = 'ps4' +PS4_DATA = 'ps4_data' # Deprecated used for logger/backwards compatibility from 0.89 REGIONS = ['R1', 'R2', 'R3', 'R4', 'R5'] diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 1cf613bf9b9..a94fcd44082 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ps4", "requirements": [ - "pyps4-homeassistant==0.7.3" + "pyps4-homeassistant==0.8.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index f5360f491db..7d51dd4463e 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -1,29 +1,31 @@ """Support for PlayStation 4 consoles.""" import logging -import socket +import asyncio import voluptuous as vol +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_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) + SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.components.ps4 import format_unique_id from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_REGION, CONF_TOKEN, STATE_IDLE, STATE_OFF, STATE_PLAYING) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import device_registry, entity_registry from homeassistant.util.json import load_json, save_json -from .const import DOMAIN as PS4_DOMAIN, REGIONS as deprecated_regions +from .const import (DEFAULT_ALIAS, DOMAIN as PS4_DOMAIN, PS4_DATA, + REGIONS as deprecated_regions) _LOGGER = logging.getLogger(__name__) SUPPORT_PS4 = SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ - SUPPORT_STOP | SUPPORT_SELECT_SOURCE + SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE -PS4_DATA = 'ps4_data' ICON = 'mdi:playstation' GAMES_FILE = '.ps4-games.json' MEDIA_IMAGE_DEFAULT = None @@ -50,35 +52,29 @@ PS4_COMMAND_SCHEMA = vol.Schema({ 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) + await async_setup_platform( + hass, config, async_add_entities, discovery_info=None) async def async_service_handle(hass): """Handle for services.""" - def service_command(call): + async def async_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) + await device.async_send_command(command) hass.services.async_register( - PS4_DOMAIN, SERVICE_COMMAND, service_command, + PS4_DOMAIN, SERVICE_COMMAND, async_service_command, schema=PS4_COMMAND_SCHEMA) await async_service_handle(hass) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up PS4 Platform.""" - import pyps4_homeassistant as pyps4 - hass.data[PS4_DATA] = PS4Data() + import pyps4_homeassistant.ps4 as pyps4 games_file = hass.config.path(GAMES_FILE) creds = config.data[CONF_TOKEN] device_list = [] @@ -86,25 +82,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = device[CONF_HOST] region = device[CONF_REGION] name = device[CONF_NAME] - ps4 = pyps4.Ps4(host, creds) + ps4 = pyps4.Ps4Async(host, creds, device_name=DEFAULT_ALIAS) device_list.append(PS4Device( - name, host, region, ps4, creds, games_file)) - add_entities(device_list, True) - - -class PS4Data(): - """Init Data Class.""" - - def __init__(self): - """Init Class.""" - self.devices = [] + config, name, host, region, ps4, creds, games_file)) + async_add_entities(device_list, update_before_add=True) class PS4Device(MediaPlayerDevice): """Representation of a PS4.""" - def __init__(self, name, host, region, ps4, creds, games_file): + def __init__(self, config, name, host, region, ps4, creds, games_file): """Initialize the ps4 device.""" + self._entry_id = config.entry_id self._ps4 = ps4 self._host = host self._name = name @@ -123,56 +112,87 @@ class PS4Device(MediaPlayerDevice): self._disconnected = False self._info = None self._unique_id = None - self._power_on = False + + @callback + def status_callback(self): + """Handle status callback. Parse status.""" + self._parse_status() + + @callback + def schedule_update(self): + """Schedules update with HA.""" + self.async_schedule_update_ha_state() + + @callback + def subscribe_to_protocol(self): + """Notify protocol to callback with update changes.""" + self.hass.data[PS4_DATA].protocol.add_callback( + self._ps4, self.status_callback) + + def check_region(self): + """Display logger msg if region is deprecated.""" + # Non-Breaking although data returned may be inaccurate. + if self._region in deprecated_regions: + _LOGGER.info("""Region: %s has been deprecated. + Please remove PS4 integration + and Re-configure again to utilize + current regions""", self._region) async def async_added_to_hass(self): """Subscribe PS4 events.""" self.hass.data[PS4_DATA].devices.append(self) + self.check_region() - def update(self): + async def async_update(self): """Retrieve the latest data.""" - try: - status = self._ps4.get_status() - if self._info is None: - # Add entity to registry - self.get_device_info(status) - self._games = self.load_games() - if self._games is not None: - self._source_list = list(sorted(self._games.values())) - # Non-Breaking although data returned may be inaccurate. - if self._region in deprecated_regions: - _LOGGER.info("""Region: %s has been deprecated. - Please remove PS4 integration - and Re-configure again to utilize - current regions""", self._region) - except socket.timeout: - status = None + if self._ps4.ddp_protocol is not None: + # Request Status with asyncio transport. + self._ps4.get_status() + if not self._ps4.connected and not self._ps4.is_standby: + await self._ps4.async_connect() + + # Try to ensure correct status is set on startup for device info. + if self._ps4.ddp_protocol is None: + # Use socket.socket. + await self.hass.async_add_executor_job(self._ps4.get_status) + self._ps4.ddp_protocol = self.hass.data[PS4_DATA].protocol + self.subscribe_to_protocol() + + if self._ps4.status is not None: + if self._info is None: + # Add entity to registry. + await self.async_get_device_info(self._ps4.status) + self._parse_status() + + def _parse_status(self): + """Parse status.""" + status = self._ps4.status + if status is not None: + self._games = self.load_games() + if self._games is not None: + self._source_list = list(sorted(self._games.values())) self._retry = 0 self._disconnected = False if status.get('status') == 'Ok': - # Check if only 1 device in Hass. - if len(self.hass.data[PS4_DATA].devices) == 1: - # Enable keep alive feature for PS4 Connection. - # Only 1 device is supported, Since have to use port 997. - self._ps4.keep_alive = True - else: - self._ps4.keep_alive = False - if self._power_on: - # Auto Login after Turn On. - self._ps4.open() - self._power_on = False 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) + self._media_title = name + self._source = self._media_title + self._media_type = None + asyncio.ensure_future( + self.async_get_title_data(title_id, name)) else: - self.idle() + if self._state != STATE_IDLE: + self.idle() else: - self.state_off() + if self._state != STATE_OFF: + self.state_off() + elif self._retry > 5: self.state_unknown() else: @@ -182,11 +202,13 @@ class PS4Device(MediaPlayerDevice): """Set states for state idle.""" self.reset_title() self._state = STATE_IDLE + self.schedule_update() def state_off(self): """Set states for state off.""" self.reset_title() self._state = STATE_OFF + self.schedule_update() def state_unknown(self): """Set states for state unknown.""" @@ -201,32 +223,47 @@ class PS4Device(MediaPlayerDevice): """Update if there is no title.""" self._media_title = None self._media_content_id = None + self._media_type = None self._source = None - def get_title_data(self, title_id, name): + async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" from pyps4_homeassistant.errors import PSDataIncomplete app_name = None art = None + media_type = None try: - title = self._ps4.get_ps_store_data( + title = await self._ps4.async_get_ps_store_data( name, title_id, self._region) + except PSDataIncomplete: - _LOGGER.error( - "Could not find data in region: %s for PS ID: %s", - self._region, title_id) + title = None + except asyncio.TimeoutError: + title = None + _LOGGER.error("PS Store Search Timed out") + else: - app_name = title.name - art = title.cover_art + if title is not None: + app_name = title.name + art = title.cover_art + # Also assume media type is game if search fails. + if title.game_type != 'App': + media_type = MEDIA_TYPE_GAME + else: + media_type = MEDIA_TYPE_APP + else: + _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 - if title.game_type == 'App': - self._media_type = MEDIA_TYPE_APP - else: - self._media_type = MEDIA_TYPE_GAME + self._media_image = art or None + self._media_type = media_type + self.update_list() + self.schedule_update() def update_list(self): """Update Game List, Correct data if different.""" @@ -234,9 +271,11 @@ class PS4Device(MediaPlayerDevice): 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): @@ -271,28 +310,50 @@ class PS4Device(MediaPlayerDevice): games.update(game) self.save_games(games) - def get_device_info(self, status): + async def async_get_device_info(self, status): """Set 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 - } + # If cannot get status on startup, assume info from registry. + if status is None: + _LOGGER.info("Assuming status from registry") + e_registry = await entity_registry.async_get_registry(self.hass) + d_registry = await device_registry.async_get_registry(self.hass) + for entity_id, entry in e_registry.entities.items(): + if entry.config_entry_id == self._entry_id: + self._unique_id = entry.unique_id + self.entity_id = entity_id + break + for device in d_registry.devices.values(): + if self._entry_id in device.config_entries: + self._info = { + 'name': device.name, + 'model': device.model, + 'identifiers': device.identifiers, + 'manufacturer': device.manufacturer, + 'sw_version': device.sw_version + } + break - self._unique_id = format_unique_id(self._creds, status['host-id']) + else: + _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 = format_unique_id(self._creds, status['host-id']) async def async_will_remove_from_hass(self): """Remove Entity from Hass.""" - # Close TCP Socket + # Close TCP Transport. if self._ps4.connected: - await self.hass.async_add_executor_job(self._ps4.close) + await self._ps4.close() self.hass.data[PS4_DATA].devices.remove(self) @property @@ -367,43 +428,44 @@ class PS4Device(MediaPlayerDevice): """List of available input sources.""" return self._source_list - def turn_off(self): + async def async_turn_off(self): """Turn off media player.""" - self._ps4.standby() + await self._ps4.standby() - def turn_on(self): + async def async_turn_on(self): """Turn on the media player.""" - self._power_on = True self._ps4.wakeup() - def media_pause(self): + async def async_media_pause(self): """Send keypress ps to return to menu.""" - self.send_remote_control('ps') + await self.async_send_remote_control('ps') - def media_stop(self): + async def async_media_stop(self): """Send keypress ps to return to menu.""" - self.send_remote_control('ps') + await self.async_send_remote_control('ps') - def select_source(self, source): + async def async_select_source(self, source): """Select input source.""" for title_id, game in self._games.items(): if source.lower().encode(encoding='utf-8') == \ game.lower().encode(encoding='utf-8') \ or source == title_id: + _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) + + await self._ps4.start_title(title_id, self._media_content_id) return + _LOGGER.warning( "Could not start title. '%s' is not in source list", source) return - def send_command(self, command): + async def async_send_command(self, command): """Send Button Command.""" - self.send_remote_control(command) + await self.async_send_remote_control(command) - def send_remote_control(self, command): + async def async_send_remote_control(self, command): """Send RC command.""" - self._ps4.remote_control(command) + await self._ps4.remote_control(command) diff --git a/requirements_all.txt b/requirements_all.txt index dd58748d512..1a10980db39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1294,7 +1294,7 @@ pypjlink2==1.2.0 pypoint==1.1.1 # homeassistant.components.ps4 -pyps4-homeassistant==0.7.3 +pyps4-homeassistant==0.8.2 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21cdf12f016..d8478353580 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -274,7 +274,7 @@ pyopenuv==1.0.9 pyotp==2.2.7 # homeassistant.components.ps4 -pyps4-homeassistant==0.7.3 +pyps4-homeassistant==0.8.2 # homeassistant.components.qwikswitch pyqwikswitch==0.93