diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json index c0b476ff4e2..662f6fb6116 100644 --- a/homeassistant/components/ps4/.translations/en.json +++ b/homeassistant/components/ps4/.translations/en.json @@ -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" } -} \ No newline at end of file +} diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index edefb5e4709..d5833ae1673 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -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 diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 148b0ae6d84..1b184a3774f 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -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( diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index 0618ca9675f..bbf654530b0 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -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'] diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 60b656a469d..e2f0004f80e 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -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 diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json index 5f4e2a7c8b4..d8fdc9e18db 100644 --- a/homeassistant/components/ps4/strings.json +++ b/homeassistant/components/ps4/strings.json @@ -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." } } } diff --git a/requirements_all.txt b/requirements_all.txt index de994fa9122..8603d7a91de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe026a3813c..1a8298d196b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 271db46d856..06fe1ef65da 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -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'}