Improve PS4 media art fetching and config flow (#22167)

* improved config flow

* Added errors, docs url

* Added errors, docs url

* Added manual config mode

* Add tests for manual/auto host input

* fix inline docs

* fix inline docs

* Changed region list

* Added deprecated region message

* removed DEFAULT_REGION

* Added close method

* Fixes

* Update const.py

* Update const.py

* Update const.py

* Update test_config_flow.py

* Added invalid pin errors

* Update strings.json

* Update strings.json

* bump pyps4 to 0.5.0

* Bump pyps4 0.5.0

* Bump pyps4 to 0.5.0

* test fixes

* pylint

* Change error reference

* remove pin messages

* remove pin messages

* Update en.json

* remove pin tests

* fix tests

* update vol

* Vol fix

* Update config_flow.py

* Add migration for v1 entry

* lint

* fixes

* typo

* fix

* Update config_flow.py

* Fix vol

* Executor job for io method.

* Update __init__.py

* blank line

* Update __init__.py

* Update tests/components/ps4/test_config_flow.py

Co-Authored-By: ktnrg45 <38207570+ktnrg45@users.noreply.github.com>
This commit is contained in:
ktnrg45 2019-03-25 05:25:15 -07:00 committed by Charles Garwood
parent 96133f5e6b
commit 17a96c6d9b
9 changed files with 227 additions and 71 deletions

View file

@ -4,18 +4,27 @@
"credential_error": "Error fetching credentials.",
"devices_configured": "All devices found are already configured.",
"no_devices_found": "No PlayStation 4 devices found on the network.",
"port_987_bind_error": "Could not bind to port 987.",
"port_997_bind_error": "Could not bind to port 997."
"port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.",
"port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info."
},
"error": {
"login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct.",
"not_ready": "PlayStation 4 is not on or connected to network."
"not_ready": "PlayStation 4 is not on or connected to network.",
"no_ipaddress": "Enter the IP Address of the PlayStation 4 you would like to configure."
},
"step": {
"creds": {
"description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue.",
"title": "PlayStation 4"
},
"mode": {
"data": {
"mode": "Config Mode",
"ip_address": "IP Address (Leave empty if using Auto Discovery)."
},
"description": "Select mode for configuration. The IP Address field can be left blank if selecting Auto Discovery, as devices will be automatically discovered.",
"title": "PlayStation 4"
},
"link": {
"data": {
"code": "PIN",
@ -23,10 +32,10 @@
"name": "Name",
"region": "Region"
},
"description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.",
"description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.",
"title": "PlayStation 4"
}
},
"title": "PlayStation 4"
}
}
}

View file

@ -6,13 +6,15 @@ https://home-assistant.io/components/ps4/
"""
import logging
from .config_flow import ( # noqa pylint: disable=unused-import
PlayStation4FlowHandler)
from homeassistant.const import CONF_REGION
from homeassistant.util import location
from .config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import
from .const import DOMAIN # noqa: pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyps4-homeassistant==0.4.8']
REQUIREMENTS = ['pyps4-homeassistant==0.5.0']
async def async_setup(hass, config):
@ -32,3 +34,40 @@ async def async_unload_entry(hass, entry):
await hass.config_entries.async_forward_entry_unload(
entry, 'media_player')
return True
async def async_migrate_entry(hass, entry):
"""Migrate old entry."""
from pyps4_homeassistant.media_art import COUNTRIES
config_entries = hass.config_entries
data = entry.data
version = entry.version
reason = {1: "Region codes have changed"} # From 0.89
# Migrate Version 1 -> Version 2
if version == 1:
loc = await hass.async_add_executor_job(location.detect_location_info)
if loc:
country = loc.country_name
if country in COUNTRIES:
for device in data['devices']:
device[CONF_REGION] = country
entry.version = 2
config_entries.async_update_entry(entry, data=data)
_LOGGER.info(
"PlayStation 4 Config Updated: \
Region changed to: %s", country)
return True
msg = """{} for the PlayStation 4 Integration.
Please remove the PS4 Integration and re-configure
[here](/config/integrations).""".format(reason[version])
hass.components.persistent_notification.async_create(
title="PlayStation 4 Integration Configuration Requires Update",
message=msg,
notification_id='config_entry_migration'
)
return False

View file

@ -8,10 +8,14 @@ from homeassistant import config_entries
from homeassistant.const import (
CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN)
from .const import DEFAULT_NAME, DEFAULT_REGION, DOMAIN, REGIONS
from .const import DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_MODE = 'Config Mode'
CONF_AUTO = "Auto Discover"
CONF_MANUAL = "Manual Entry"
UDP_PORT = 987
TCP_PORT = 997
PORT_MSG = {UDP_PORT: 'port_987_bind_error', TCP_PORT: 'port_997_bind_error'}
@ -21,7 +25,7 @@ PORT_MSG = {UDP_PORT: 'port_987_bind_error', TCP_PORT: 'port_997_bind_error'}
class PlayStation4FlowHandler(config_entries.ConfigFlow):
"""Handle a PlayStation 4 config flow."""
VERSION = 1
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
@ -34,6 +38,8 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
self.host = None
self.region = None
self.pin = None
self.m_device = None
self.device_list = []
async def async_step_user(self, user_input=None):
"""Handle a user config flow."""
@ -46,7 +52,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
return self.async_abort(reason=reason)
# Skip Creds Step if a device is configured.
if self.hass.config_entries.async_entries(DOMAIN):
return await self.async_step_link()
return await self.async_step_mode()
return await self.async_step_creds()
async def async_step_creds(self, user_input=None):
@ -56,53 +62,82 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
self.helper.get_creds)
if self.creds is not None:
return await self.async_step_link()
return await self.async_step_mode()
return self.async_abort(reason='credential_error')
return self.async_show_form(
step_id='creds')
async def async_step_mode(self, user_input=None):
"""Prompt for mode."""
errors = {}
mode = [CONF_AUTO, CONF_MANUAL]
if user_input is not None:
if user_input[CONF_MODE] == CONF_MANUAL:
try:
device = user_input[CONF_IP_ADDRESS]
if device:
self.m_device = device
except KeyError:
errors[CONF_IP_ADDRESS] = 'no_ipaddress'
if not errors:
return await self.async_step_link()
mode_schema = OrderedDict()
mode_schema[vol.Required(
CONF_MODE, default=CONF_AUTO)] = vol.In(list(mode))
mode_schema[vol.Optional(CONF_IP_ADDRESS)] = str
return self.async_show_form(
step_id='mode',
data_schema=vol.Schema(mode_schema),
errors=errors,
)
async def async_step_link(self, user_input=None):
"""Prompt user input. Create or edit entry."""
from pyps4_homeassistant.media_art import COUNTRIES
regions = sorted(COUNTRIES.keys())
errors = {}
# Search for device.
devices = await self.hass.async_add_executor_job(
self.helper.has_devices)
if user_input is None:
# Search for device.
devices = await self.hass.async_add_executor_job(
self.helper.has_devices, self.m_device)
# Abort if can't find device.
if not devices:
return self.async_abort(reason='no_devices_found')
# Abort if can't find device.
if not devices:
return self.async_abort(reason='no_devices_found')
device_list = [
device['host-ip'] for device in devices]
self.device_list = [device['host-ip'] for device in devices]
# If entry exists check that devices found aren't configured.
if self.hass.config_entries.async_entries(DOMAIN):
creds = {}
for entry in self.hass.config_entries.async_entries(DOMAIN):
# Retrieve creds from entry
creds['data'] = entry.data[CONF_TOKEN]
# Retrieve device data from entry
conf_devices = entry.data['devices']
for c_device in conf_devices:
if c_device['host'] in device_list:
# Remove configured device from search list.
device_list.remove(c_device['host'])
# If list is empty then all devices are configured.
if not device_list:
return self.async_abort(reason='devices_configured')
# Add existing creds for linking. Should be only 1.
if not creds:
# Abort if creds is missing.
return self.async_abort(reason='credential_error')
self.creds = creds['data']
# If entry exists check that devices found aren't configured.
if self.hass.config_entries.async_entries(DOMAIN):
creds = {}
for entry in self.hass.config_entries.async_entries(DOMAIN):
# Retrieve creds from entry
creds['data'] = entry.data[CONF_TOKEN]
# Retrieve device data from entry
conf_devices = entry.data['devices']
for c_device in conf_devices:
if c_device['host'] in self.device_list:
# Remove configured device from search list.
self.device_list.remove(c_device['host'])
# If list is empty then all devices are configured.
if not self.device_list:
return self.async_abort(reason='devices_configured')
# Add existing creds for linking. Should be only 1.
if not creds:
# Abort if creds is missing.
return self.async_abort(reason='credential_error')
self.creds = creds['data']
# Login to PS4 with user data.
if user_input is not None:
self.region = user_input[CONF_REGION]
self.name = user_input[CONF_NAME]
self.pin = user_input[CONF_CODE]
self.pin = str(user_input[CONF_CODE])
self.host = user_input[CONF_IP_ADDRESS]
is_ready, is_login = await self.hass.async_add_executor_job(
@ -130,10 +165,11 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
# Show User Input form.
link_schema = OrderedDict()
link_schema[vol.Required(CONF_IP_ADDRESS)] = vol.In(list(device_list))
link_schema[vol.Required(
CONF_REGION, default=DEFAULT_REGION)] = vol.In(list(REGIONS))
link_schema[vol.Required(CONF_CODE)] = str
link_schema[vol.Required(CONF_IP_ADDRESS)] = vol.In(
list(self.device_list))
link_schema[vol.Required(CONF_REGION)] = vol.In(list(regions))
link_schema[vol.Required(CONF_CODE)] = vol.All(
vol.Strip, vol.Length(min=8, max=8), vol.Coerce(int))
link_schema[vol.Required(CONF_NAME, default=DEFAULT_NAME)] = str
return self.async_show_form(

View file

@ -1,5 +1,7 @@
"""Constants for PlayStation 4."""
DEFAULT_NAME = "PlayStation 4"
DEFAULT_REGION = "R1"
DEFAULT_REGION = "United States"
DOMAIN = 'ps4'
REGIONS = ('R1', 'R2', 'R3', 'R4', 'R5')
# Deprecated used for logger/backwards compatibility from 0.89
REGIONS = ['R1', 'R2', 'R3', 'R4', 'R5']

View file

@ -20,7 +20,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from .const import DOMAIN as PS4_DOMAIN
from .const import DOMAIN as PS4_DOMAIN, REGIONS as deprecated_regions
DEPENDENCIES = ['ps4']
@ -142,6 +142,12 @@ class PS4Device(MediaPlayerDevice):
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 status is not None:
@ -275,6 +281,8 @@ class PS4Device(MediaPlayerDevice):
async def async_will_remove_from_hass(self):
"""Remove Entity from Hass."""
# Close TCP Socket
await self.hass.async_add_executor_job(self._ps4.close)
self.hass.data[PS4_DATA].devices.remove(self)
@property
@ -320,6 +328,7 @@ class PS4Device(MediaPlayerDevice):
@property
def media_content_type(self):
"""Content type of current playing media."""
# No MEDIA_TYPE_GAME attr as of 0.90.
return MEDIA_TYPE_MUSIC
@property

View file

@ -6,9 +6,17 @@
"title": "PlayStation 4",
"description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue."
},
"mode": {
"title": "PlayStation 4",
"description": "Select mode for configuration. The IP Address field can be left blank if selecting Auto Discovery, as devices will be automatically discovered.",
"data": {
"mode": "Config Mode",
"ip_address": "IP Address (Leave empty if using Auto Discovery)."
},
},
"link": {
"title": "PlayStation 4",
"description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.",
"description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.",
"data": {
"region": "Region",
"name": "Name",
@ -19,14 +27,15 @@
},
"error": {
"not_ready": "PlayStation 4 is not on or connected to network.",
"login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct."
"login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct.",
"no_ipaddress": "Enter the IP Address of the PlayStation 4 you would like to configure."
},
"abort": {
"credential_error": "Error fetching credentials.",
"no_devices_found": "No PlayStation 4 devices found on the network.",
"devices_configured": "All devices found are already configured.",
"port_987_bind_error": "Could not bind to port 987.",
"port_997_bind_error": "Could not bind to port 997."
"devices_configured": "All devices found are already configured.",
"port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.",
"port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info."
}
}
}

View file

@ -1232,7 +1232,7 @@ pypoint==1.1.1
pypollencom==2.2.3
# homeassistant.components.ps4
pyps4-homeassistant==0.4.8
pyps4-homeassistant==0.5.0
# homeassistant.components.qwikswitch
pyqwikswitch==0.8

View file

@ -230,7 +230,7 @@ pyopenuv==1.0.9
pyotp==2.2.6
# homeassistant.components.ps4
pyps4-homeassistant==0.4.8
pyps4-homeassistant==0.5.0
# homeassistant.components.qwikswitch
pyqwikswitch==0.8

View file

@ -44,6 +44,9 @@ MOCK_DATA = {
MOCK_UDP_PORT = int(987)
MOCK_TCP_PORT = int(997)
MOCK_AUTO = {"Config Mode": 'Auto Discover'}
MOCK_MANUAL = {"Config Mode": 'Manual Entry', CONF_IP_ADDRESS: MOCK_HOST}
async def test_full_flow_implementation(hass):
"""Test registering an implementation and flow works."""
@ -58,13 +61,18 @@ async def test_full_flow_implementation(hass):
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'creds'
# Step Creds results with form in Step Link.
# Step Creds results with form in Step Mode.
with patch('pyps4_homeassistant.Helper.get_creds',
return_value=MOCK_CREDS), \
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST}]):
return_value=MOCK_CREDS):
result = await flow.async_step_creds({})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'mode'
# Step Mode with User Input which is not manual, results in Step Link.
with patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST}]):
result = await flow.async_step_mode(MOCK_AUTO)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
# User Input results in created entry.
@ -78,6 +86,8 @@ async def test_full_flow_implementation(hass):
assert result['data']['devices'] == [MOCK_DEVICE]
assert result['title'] == MOCK_TITLE
await hass.async_block_till_done()
# Add entry using result data.
mock_data = {
CONF_TOKEN: result['data'][CONF_TOKEN],
@ -104,14 +114,19 @@ async def test_multiple_flow_implementation(hass):
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'creds'
# Step Creds results with form in Step Link.
# Step Creds results with form in Step Mode.
with patch('pyps4_homeassistant.Helper.get_creds',
return_value=MOCK_CREDS), \
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST},
{'host-ip': MOCK_HOST_ADDITIONAL}]):
return_value=MOCK_CREDS):
result = await flow.async_step_creds({})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'mode'
# Step Mode with User Input which is not manual, results in Step Link.
with patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST},
{'host-ip': MOCK_HOST_ADDITIONAL}]):
result = await flow.async_step_mode(MOCK_AUTO)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
# User Input results in created entry.
@ -142,7 +157,7 @@ async def test_multiple_flow_implementation(hass):
# Test additional flow.
# User Step Started, results in Step Link:
# User Step Started, results in Step Mode:
with patch('pyps4_homeassistant.Helper.port_bind',
return_value=None), \
patch('pyps4_homeassistant.Helper.has_devices',
@ -150,6 +165,14 @@ async def test_multiple_flow_implementation(hass):
{'host-ip': MOCK_HOST_ADDITIONAL}]):
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'mode'
# Step Mode with User Input which is not manual, results in Step Link.
with patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST},
{'host-ip': MOCK_HOST_ADDITIONAL}]):
result = await flow.async_step_mode(MOCK_AUTO)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
# Step Link
@ -158,12 +181,14 @@ async def test_multiple_flow_implementation(hass):
{'host-ip': MOCK_HOST_ADDITIONAL}]), \
patch('pyps4_homeassistant.Helper.link',
return_value=(True, True)):
result = await flow.async_step_link(user_input=MOCK_CONFIG_ADDITIONAL)
result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL)
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data'][CONF_TOKEN] == MOCK_CREDS
assert len(result['data']['devices']) == 1
assert result['title'] == MOCK_TITLE
await hass.async_block_till_done()
mock_data = {
CONF_TOKEN: result['data'][CONF_TOKEN],
'devices': result['data']['devices']}
@ -230,7 +255,7 @@ async def test_additional_device(hass):
{'host-ip': MOCK_HOST_ADDITIONAL}]), \
patch('pyps4_homeassistant.Helper.link',
return_value=(True, True)):
result = await flow.async_step_link(user_input=MOCK_CONFIG_ADDITIONAL)
result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL)
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data'][CONF_TOKEN] == MOCK_CREDS
assert len(result['data']['devices']) == 1
@ -249,12 +274,26 @@ async def test_no_devices_found_abort(hass):
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
with patch('pyps4_homeassistant.Helper.has_devices', return_value=None):
result = await flow.async_step_link(MOCK_CONFIG)
with patch('pyps4_homeassistant.Helper.has_devices', return_value=[]):
result = await flow.async_step_link()
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_devices_found'
async def test_manual_mode(hass):
"""Test host specified in manual mode is passed to Step Link."""
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
# Step Mode with User Input: manual, results in Step Link.
with patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': flow.m_device}]):
result = await flow.async_step_mode(MOCK_MANUAL)
assert flow.m_device == MOCK_HOST
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
async def test_credential_abort(hass):
"""Test that failure to get credentials aborts flow."""
flow = ps4.PlayStation4FlowHandler()
@ -266,8 +305,8 @@ async def test_credential_abort(hass):
assert result['reason'] == 'credential_error'
async def test_invalid_pin_error(hass):
"""Test that invalid pin throws an error."""
async def test_wrong_pin_error(hass):
"""Test that incorrect pin throws an error."""
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
@ -294,3 +333,16 @@ async def test_device_connection_error(hass):
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
assert result['errors'] == {'base': 'not_ready'}
async def test_manual_mode_no_ip_error(hass):
"""Test no IP specified in manual mode throws an error."""
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
mock_input = {"Config Mode": 'Manual Entry'}
result = await flow.async_step_mode(mock_input)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'mode'
assert result['errors'] == {CONF_IP_ADDRESS: 'no_ipaddress'}