diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 51260f5d86e..087d3f89f80 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -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): diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 3557c3fd930..d000ed1f7e7 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -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] diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index bf7be1bbf91..74dce515d9d 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index 8166a1e9699..ad42b78eb78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30a303ac871..bb36f50444e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index b0170beeb48..271db46d856 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -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'}