WIP Ps4 Convert entity to Async / Fix entity name changing (#24101)
* Convert ps4 to async * Init client handler. * Add PS4_DATA * Move data class * add handler * add import * Update __init__.py * Change most functions to async * bump 0.8.0 * bump 0.8.0 * bump 0.8.0 * Pylint * whitespace * Rewrite to use asyncio sockets. * Remove unneeded log * Add alias * Update __init__.py * Update config_flow.py * Add alias * Add search_all method * Clean up * whitespace * change comment * 0.8.2 * 0.8.2 * 0.8.2 * Pylint * pylint * faster updates * Avoid scheduling update if state is the same. * Better handling remove search allpull/24528/head
parent
6c5124e12a
commit
8951c80225
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue