Add support for multiple devices for PS4 component (#21302)

* Support multiple devices.

* Revert "Support multiple devices."

This reverts commit 3f5d4462a98da13ebb1ab1c07d341dbd7020e6cc.

* Support multiple devices

* Bump to 0.3.3

* bump 0.3.4

* Add tests for multiple devices.

* Update Requirements

* Update config_flow.py

* Update config_flow.py

* fixed typo

* Reordered functions

* Added multiple flow implementation test.

* fix

* typo

* fix tests

* bump 0.4.0

* Bump 0.4.0

* 0.4.0

* bump version

* bump version

* bump version

* Add keep alive feature with multiple devices

* bump version

* bump version

* bump version

* bump 0.4.7

* bump 0.4.7

* bump 0.4.7

* Edited tests.

* bump/pylint

* pylint

* bump/pylint

* bump/pylint

* Change to add additional entry

* Changed to multiple entries

* pylint

* Corrections to manage multiple devices.

* lint

* comments

* Removed redundant for loop

* Shorthand correction

* Remove reference to private object

* Test fix

* Revert changes. Test failure.

* Test fix

* test fix

* unindent assertions

* pylint
This commit is contained in:
ktnrg45 2019-03-04 17:48:25 -07:00 committed by Martin Hjelmare
parent df25128923
commit efa5d5dfe3
6 changed files with 214 additions and 34 deletions

View file

@ -11,7 +11,7 @@ from homeassistant.components.ps4.const import DOMAIN # noqa: pylint: disable=u
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyps4-homeassistant==0.3.0']
REQUIREMENTS = ['pyps4-homeassistant==0.4.8']
async def async_setup(hass, config):

View file

@ -37,10 +37,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
async def async_step_user(self, user_input=None):
"""Handle a user config flow."""
# Abort if device is configured.
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='devices_configured')
# Check if able to bind to ports: UDP 987, TCP 997.
ports = PORT_MSG.keys()
failed = await self.hass.async_add_executor_job(
@ -48,6 +44,9 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
if failed in ports:
reason = PORT_MSG[failed]
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_creds()
async def async_step_creds(self, user_input=None):
@ -78,6 +77,18 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow):
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):
for entry in self.hass.config_entries.async_entries(DOMAIN):
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')
# Login to PS4 with user data.
if user_input is not None:
self.region = user_input[CONF_REGION]

View file

@ -133,6 +133,7 @@ class PS4Device(MediaPlayerDevice):
self._retry = 0
self._info = None
self._unique_id = None
self._power_on = False
async def async_added_to_hass(self):
"""Subscribe PS4 events."""
@ -144,6 +145,7 @@ class PS4Device(MediaPlayerDevice):
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:
@ -153,6 +155,17 @@ class PS4Device(MediaPlayerDevice):
if status is not None:
self._retry = 0
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:
@ -268,6 +281,10 @@ class PS4Device(MediaPlayerDevice):
}
self._unique_id = status['host-id']
async def async_will_remove_from_hass(self):
"""Remove Entity from Hass."""
self.hass.data[PS4_DATA].devices.remove(self)
@property
def device_info(self):
"""Return information about the device."""
@ -346,15 +363,16 @@ class PS4Device(MediaPlayerDevice):
def turn_on(self):
"""Turn on the media player."""
self._power_on = True
self._ps4.wakeup()
def media_pause(self):
"""Send keypress ps to return to menu."""
self._ps4.remote_control('ps')
self.send_remote_control('ps')
def media_stop(self):
"""Send keypress ps to return to menu."""
self._ps4.remote_control('ps')
self.send_remote_control('ps')
def select_source(self, source):
"""Select input source."""
@ -369,4 +387,8 @@ class PS4Device(MediaPlayerDevice):
def send_command(self, command):
"""Send Button Command."""
self.send_remote_control(command)
def send_remote_control(self, command):
"""Send RC command."""
self._ps4.remote_control(command)

View file

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

View file

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

View file

@ -14,20 +14,32 @@ MOCK_TITLE = 'PlayStation 4'
MOCK_CODE = '12345678'
MOCK_CREDS = '000aa000'
MOCK_HOST = '192.0.0.0'
MOCK_HOST_ADDITIONAL = '192.0.0.1'
MOCK_DEVICE = {
CONF_HOST: MOCK_HOST,
CONF_NAME: DEFAULT_NAME,
CONF_REGION: DEFAULT_REGION
}
MOCK_DEVICE_ADDITIONAL = {
CONF_HOST: MOCK_HOST_ADDITIONAL,
CONF_NAME: DEFAULT_NAME,
CONF_REGION: DEFAULT_REGION
}
MOCK_CONFIG = {
CONF_IP_ADDRESS: MOCK_HOST,
CONF_NAME: DEFAULT_NAME,
CONF_REGION: DEFAULT_REGION,
CONF_CODE: MOCK_CODE
}
MOCK_CONFIG_ADDITIONAL = {
CONF_IP_ADDRESS: MOCK_HOST_ADDITIONAL,
CONF_NAME: DEFAULT_NAME,
CONF_REGION: DEFAULT_REGION,
CONF_CODE: MOCK_CODE
}
MOCK_DATA = {
CONF_TOKEN: MOCK_CREDS,
'devices': MOCK_DEVICE
'devices': [MOCK_DEVICE]
}
MOCK_UDP_PORT = int(987)
MOCK_TCP_PORT = int(997)
@ -37,13 +49,14 @@ async def test_full_flow_implementation(hass):
"""Test registering an implementation and flow works."""
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
manager = hass.config_entries
# User Step Started, results in Step Creds
with patch('pyps4_homeassistant.Helper.port_bind',
return_value=None):
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'creds'
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'creds'
# Step Creds results with form in Step Link.
with patch('pyps4_homeassistant.Helper.get_creds',
@ -51,8 +64,8 @@ async def test_full_flow_implementation(hass):
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST}]):
result = await flow.async_step_creds({})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
# User Input results in created entry.
with patch('pyps4_homeassistant.Helper.link',
@ -60,10 +73,110 @@ async def test_full_flow_implementation(hass):
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST}]):
result = await flow.async_step_link(MOCK_CONFIG)
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data'][CONF_TOKEN] == MOCK_CREDS
assert result['data']['devices'] == [MOCK_DEVICE]
assert result['title'] == MOCK_TITLE
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data'][CONF_TOKEN] == MOCK_CREDS
assert result['data']['devices'] == [MOCK_DEVICE]
assert result['title'] == MOCK_TITLE
# Add entry using result data.
mock_data = {
CONF_TOKEN: result['data'][CONF_TOKEN],
'devices': result['data']['devices']}
entry = MockConfigEntry(domain=ps4.DOMAIN, data=mock_data)
entry.add_to_manager(manager)
# Check if entry exists.
assert len(manager.async_entries()) == 1
# Check if there is a device config in entry.
assert len(entry.data['devices']) == 1
async def test_multiple_flow_implementation(hass):
"""Test multiple device flows."""
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
manager = hass.config_entries
# User Step Started, results in Step Creds
with patch('pyps4_homeassistant.Helper.port_bind',
return_value=None):
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'creds'
# Step Creds results with form in Step Link.
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}]):
result = await flow.async_step_creds({})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
# User Input results in created entry.
with patch('pyps4_homeassistant.Helper.link',
return_value=(True, True)), \
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST},
{'host-ip': MOCK_HOST_ADDITIONAL}]):
result = await flow.async_step_link(MOCK_CONFIG)
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data'][CONF_TOKEN] == MOCK_CREDS
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],
'devices': result['data']['devices']}
entry = MockConfigEntry(domain=ps4.DOMAIN, data=mock_data)
entry.add_to_manager(manager)
# Check if entry exists.
assert len(manager.async_entries()) == 1
# Check if there is a device config in entry.
assert len(entry.data['devices']) == 1
# Test additional flow.
# User Step Started, results in Step Link:
with patch('pyps4_homeassistant.Helper.port_bind',
return_value=None), \
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST},
{'host-ip': MOCK_HOST_ADDITIONAL}]):
result = await flow.async_step_user()
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
# Step Link
with patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST},
{'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)
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
mock_data = {
CONF_TOKEN: result['data'][CONF_TOKEN],
'devices': result['data']['devices']}
# Update config entries with result data
entry = MockConfigEntry(domain=ps4.DOMAIN, data=mock_data)
entry.add_to_manager(manager)
manager.async_update_entry(entry)
# Check if there are 2 entries.
assert len(manager.async_entries()) == 2
# Check if there is device config in entry.
assert len(entry.data['devices']) == 1
async def test_port_bind_abort(hass):
@ -75,28 +188,62 @@ async def test_port_bind_abort(hass):
return_value=MOCK_UDP_PORT):
reason = 'port_987_bind_error'
result = await flow.async_step_user(user_input=None)
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == reason
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == reason
with patch('pyps4_homeassistant.Helper.port_bind',
return_value=MOCK_TCP_PORT):
reason = 'port_997_bind_error'
result = await flow.async_step_user(user_input=None)
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == reason
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == reason
async def test_duplicate_abort(hass):
"""Test that Flow aborts when already configured."""
"""Test that Flow aborts when found devices already configured."""
MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA).add_to_hass(hass)
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=None)
with patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST}]):
result = await flow.async_step_link(user_input=None)
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'devices_configured'
async def test_additional_device(hass):
"""Test that Flow can configure another device."""
flow = ps4.PlayStation4FlowHandler()
flow.hass = hass
flow.creds = MOCK_CREDS
manager = hass.config_entries
# Mock existing entry.
entry = MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA)
entry.add_to_manager(manager)
# Check that only 1 entry exists
assert len(manager.async_entries()) == 1
with patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST},
{'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)
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
# Add New Entry
entry = MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA)
entry.add_to_manager(manager)
# Check that there are 2 entries
assert len(manager.async_entries()) == 2
async def test_no_devices_found_abort(hass):
"""Test that failure to find devices aborts flow."""
flow = ps4.PlayStation4FlowHandler()
@ -104,8 +251,8 @@ async def test_no_devices_found_abort(hass):
with patch('pyps4_homeassistant.Helper.has_devices', return_value=None):
result = await flow.async_step_link(MOCK_CONFIG)
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_devices_found'
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'no_devices_found'
async def test_credential_abort(hass):
@ -115,8 +262,8 @@ async def test_credential_abort(hass):
with patch('pyps4_homeassistant.Helper.get_creds', return_value=None):
result = await flow.async_step_creds({})
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'credential_error'
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'credential_error'
async def test_invalid_pin_error(hass):
@ -129,9 +276,9 @@ async def test_invalid_pin_error(hass):
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST}]):
result = await flow.async_step_link(MOCK_CONFIG)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
assert result['errors'] == {'base': 'login_failed'}
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
assert result['errors'] == {'base': 'login_failed'}
async def test_device_connection_error(hass):
@ -144,6 +291,6 @@ async def test_device_connection_error(hass):
patch('pyps4_homeassistant.Helper.has_devices',
return_value=[{'host-ip': MOCK_HOST}]):
result = await flow.async_step_link(MOCK_CONFIG)
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
assert result['errors'] == {'base': 'not_ready'}
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'link'
assert result['errors'] == {'base': 'not_ready'}