From 7ff14b47a83e7449c01eb81b0e28f183e80a57c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 23 May 2021 05:34:48 +0200 Subject: [PATCH] Use whoami for location lookup (#50934) --- homeassistant/components/ps4/__init__.py | 11 ++- homeassistant/components/ps4/config_flow.py | 10 ++- homeassistant/components/ps4/const.py | 68 ++++++++++++++++++ homeassistant/util/location.py | 52 +++----------- tests/components/config/test_core.py | 1 - tests/components/ps4/test_config_flow.py | 1 - tests/components/ps4/test_init.py | 1 - tests/fixtures/ip-api.com.json | 16 ----- tests/fixtures/ipapi.co.json | 20 ------ tests/fixtures/whoami.json | 14 ++++ tests/util/test_location.py | 79 ++++----------------- 11 files changed, 121 insertions(+), 152 deletions(-) delete mode 100644 tests/fixtures/ip-api.com.json delete mode 100644 tests/fixtures/ipapi.co.json create mode 100644 tests/fixtures/whoami.json diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 65940b9dc48..bf5eafa7bbb 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -25,7 +25,14 @@ from homeassistant.util import location from homeassistant.util.json import load_json, save_json from .config_flow import PlayStation4FlowHandler # noqa: F401 -from .const import ATTR_MEDIA_IMAGE_URL, COMMANDS, DOMAIN, GAMES_FILE, PS4_DATA +from .const import ( + ATTR_MEDIA_IMAGE_URL, + COMMANDS, + COUNTRYCODE_NAMES, + DOMAIN, + GAMES_FILE, + PS4_DATA, +) _LOGGER = logging.getLogger(__name__) @@ -91,7 +98,7 @@ async def async_migrate_entry(hass, entry): hass.helpers.aiohttp_client.async_get_clientsession() ) if loc: - country = loc.country_name + country = COUNTRYCODE_NAMES.get(loc.country_code) if country in COUNTRIES: for device in data["devices"]: device[CONF_REGION] = country diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 1be879df58e..7424e0f5e1a 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -17,7 +17,13 @@ from homeassistant.const import ( ) from homeassistant.util import location -from .const import CONFIG_ENTRY_VERSION, DEFAULT_ALIAS, DEFAULT_NAME, DOMAIN +from .const import ( + CONFIG_ENTRY_VERSION, + COUNTRYCODE_NAMES, + DEFAULT_ALIAS, + DEFAULT_NAME, + DOMAIN, +) CONF_MODE = "Config Mode" CONF_AUTO = "Auto Discover" @@ -178,7 +184,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.helpers.aiohttp_client.async_get_clientsession() ) if self.location: - country = self.location.country_name + country = COUNTRYCODE_NAMES.get(self.location.country_code) if country in COUNTRIES: default_region = country diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index 0974286ebe8..f2d284daa79 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -12,3 +12,71 @@ COMMANDS = ("up", "down", "right", "left", "enter", "back", "option", "ps", "ps_ # Deprecated used for logger/backwards compatibility from 0.89 REGIONS = ["R1", "R2", "R3", "R4", "R5"] + +COUNTRYCODE_NAMES = { + "AE": "United Arab Emirates", + "AR": "Argentina", + "AT": "Austria", + "AU": "Australia", + "BE": "Belgium", + "BG": "Bulgaria", + "BH": "Bahrain", + "BR": "Brazil", + "CA": "Canada", + "CH": "Switzerland", + "CL": "Chile", + "CO": "Columbia", + "CR": "Costa Rica", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DE": "Germany", + "DK": "Denmark", + "EC": "Ecuador", + "ES": "Spain", + "FI": "Finland", + "FR": "France", + "GB": "United Kingdom", + "GR": "Greece", + "GT": "Guatemala", + "HK": "Hong Kong", + "HN": "Honduras", + "HR": "Croatia", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IN": "India", + "IS": "Iceland", + "IT": "Italy", + "JP": "Japan", + "KW": "Kuwait", + "LB": "Lebanon", + "LU": "Luxembourg", + "MT": "Malta", + "MX": "Mexico", + "MY": "Maylasia", + "NI": "Nicaragua", + "NL": "Nederland", + "NO": "Norway", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PL": "Poland", + "PT": "Portugal", + "QA": "Qatar", + "RO": "Romania", + "RU": "Russia", + "SA": "Saudi Arabia", + "SE": "Sweden", + "SG": "Singapore", + "SI": "Slovenia", + "SK": "Slovakia", + "SV": "El Salvador", + "TH": "Thailand", + "TR": "Turkey", + "TW": "Taiwan", + "UA": "Ukraine", + "US": "United States", + "ZA": "South Africa", +} diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index c22f5213130..2a3a4ff0922 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -12,9 +12,7 @@ from typing import Any import aiohttp -ELEVATION_URL = "https://api.open-elevation.com/api/v1/lookup" -IP_API = "http://ip-api.com/json" -IPAPI = "https://ipapi.co/json/" +WHOAMI_URL = "https://whoami.home-assistant.io/v1" # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 @@ -34,7 +32,6 @@ LocationInfo = collections.namedtuple( [ "ip", "country_code", - "country_name", "region_code", "region_name", "city", @@ -51,10 +48,7 @@ async def async_detect_location_info( session: aiohttp.ClientSession, ) -> LocationInfo | None: """Detect location information.""" - data = await _get_ipapi(session) - - if data is None: - data = await _get_ip_api(session) + data = await _get_whoami(session) if data is None: return None @@ -164,10 +158,10 @@ def vincenty( return round(s, 6) -async def _get_ipapi(session: aiohttp.ClientSession) -> dict[str, Any] | None: - """Query ipapi.co for location data.""" +async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None: + """Query whoami.home-assistant.io for location data.""" try: - resp = await session.get(IPAPI, timeout=5) + resp = await session.get(WHOAMI_URL, timeout=30) except (aiohttp.ClientError, asyncio.TimeoutError): return None @@ -176,44 +170,14 @@ async def _get_ipapi(session: aiohttp.ClientSession) -> dict[str, Any] | None: except (aiohttp.ClientError, ValueError): return None - # ipapi allows 30k free requests/month. Some users exhaust those. - if raw_info.get("latitude") == "Sign up to access": - return None - return { "ip": raw_info.get("ip"), "country_code": raw_info.get("country"), - "country_name": raw_info.get("country_name"), "region_code": raw_info.get("region_code"), "region_name": raw_info.get("region"), "city": raw_info.get("city"), - "zip_code": raw_info.get("postal"), + "zip_code": raw_info.get("postal_code"), "time_zone": raw_info.get("timezone"), - "latitude": raw_info.get("latitude"), - "longitude": raw_info.get("longitude"), - } - - -async def _get_ip_api(session: aiohttp.ClientSession) -> dict[str, Any] | None: - """Query ip-api.com for location data.""" - try: - resp = await session.get(IP_API, timeout=5) - except (aiohttp.ClientError, asyncio.TimeoutError): - return None - - try: - raw_info = await resp.json() - except (aiohttp.ClientError, ValueError): - return None - return { - "ip": raw_info.get("query"), - "country_code": raw_info.get("countryCode"), - "country_name": raw_info.get("country"), - "region_code": raw_info.get("region"), - "region_name": raw_info.get("regionName"), - "city": raw_info.get("city"), - "zip_code": raw_info.get("zip"), - "time_zone": raw_info.get("timezone"), - "latitude": raw_info.get("lat"), - "longitude": raw_info.get("lon"), + "latitude": float(raw_info.get("latitude")), + "longitude": float(raw_info.get("longitude")), } diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index b58b572e230..738b1183c14 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -144,7 +144,6 @@ async def test_detect_config_fail(hass, client): return_value=location.LocationInfo( ip=None, country_code=None, - country_name=None, region_code=None, region_name=None, city=None, diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index f8c28d236be..36c38a62b4c 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -64,7 +64,6 @@ MOCK_MANUAL = {"Config Mode": "Manual Entry", CONF_IP_ADDRESS: MOCK_HOST} MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", - "United States", "CA", "California", "San Diego", diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 94167528b21..f57fada8c37 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -56,7 +56,6 @@ MOCK_CONFIG = MockConfigEntry(domain=DOMAIN, data=MOCK_DATA, entry_id=MOCK_ENTRY MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", - "United States", "CA", "California", "San Diego", diff --git a/tests/fixtures/ip-api.com.json b/tests/fixtures/ip-api.com.json deleted file mode 100644 index d31d4560589..00000000000 --- a/tests/fixtures/ip-api.com.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "as": "AS20001 Time Warner Cable Internet LLC", - "city": "San Diego", - "country": "United States", - "countryCode": "US", - "isp": "Time Warner Cable", - "lat": 32.8594, - "lon": -117.2073, - "org": "Time Warner Cable", - "query": "1.2.3.4", - "region": "CA", - "regionName": "California", - "status": "success", - "timezone": "America\/Los_Angeles", - "zip": "92122" -} diff --git a/tests/fixtures/ipapi.co.json b/tests/fixtures/ipapi.co.json deleted file mode 100644 index f1dc58a756b..00000000000 --- a/tests/fixtures/ipapi.co.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "ip": "1.2.3.4", - "city": "Bern", - "region": "Bern", - "region_code": "BE", - "country": "CH", - "country_name": "Switzerland", - "continent_code": "EU", - "in_eu": false, - "postal": "3000", - "latitude": 46.9480278, - "longitude": 7.4490812, - "timezone": "Europe/Zurich", - "utc_offset": "+0100", - "country_calling_code": "+41", - "currency": "CHF", - "languages": "de-CH,fr-CH,it-CH,rm", - "asn": "AS6830", - "org": "Liberty Global B.V." -} \ No newline at end of file diff --git a/tests/fixtures/whoami.json b/tests/fixtures/whoami.json new file mode 100644 index 00000000000..c805ef30558 --- /dev/null +++ b/tests/fixtures/whoami.json @@ -0,0 +1,14 @@ +{ + "ip": "1.2.3.4", + "city": "Gotham", + "continent": "Earth", + "country": "XX", + "latitude": "12.34567", + "longitude": "12.34567", + "postal_code": "12345", + "region_code": "00", + "region": "Gotham", + "timezone": "Earth/Gotham", + "iso_time": "2021-05-12T11:29:15.752Z", + "timestamp": 1620818956 +} \ No newline at end of file diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 9eb2dc70561..21531a59194 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,5 +1,5 @@ """Test Home Assistant location util methods.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock import aiohttp import pytest @@ -72,76 +72,25 @@ def test_get_miles(): assert round(miles, 2) == DISTANCE_MILES -async def test_detect_location_info_ipapi(aioclient_mock, session): - """Test detect location info using ipapi.co.""" - aioclient_mock.get(location_util.IPAPI, text=load_fixture("ipapi.co.json")) +async def test_detect_location_info_whoami(aioclient_mock, session): + """Test detect location info using whoami.home-assistant.io.""" + aioclient_mock.get(location_util.WHOAMI_URL, text=load_fixture("whoami.json")) info = await location_util.async_detect_location_info(session, _test_real=True) assert info is not None assert info.ip == "1.2.3.4" - assert info.country_code == "CH" - assert info.country_name == "Switzerland" - assert info.region_code == "BE" - assert info.region_name == "Bern" - assert info.city == "Bern" - assert info.zip_code == "3000" - assert info.time_zone == "Europe/Zurich" - assert info.latitude == 46.9480278 - assert info.longitude == 7.4490812 + assert info.country_code == "XX" + assert info.region_code == "00" + assert info.city == "Gotham" + assert info.zip_code == "12345" + assert info.time_zone == "Earth/Gotham" + assert info.latitude == 12.34567 + assert info.longitude == 12.34567 assert info.use_metric -async def test_detect_location_info_ipapi_exhaust(aioclient_mock, session): - """Test detect location info using ipapi.co.""" - aioclient_mock.get(location_util.IPAPI, json={"latitude": "Sign up to access"}) - aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) - - info = await location_util.async_detect_location_info(session, _test_real=True) - - assert info is not None - # ip_api result because ipapi got skipped - assert info.country_code == "US" - assert len(aioclient_mock.mock_calls) == 2 - - -async def test_detect_location_info_ip_api(aioclient_mock, session): - """Test detect location info using ip-api.com.""" - aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) - - with patch("homeassistant.util.location._get_ipapi", return_value=None): - info = await location_util.async_detect_location_info(session, _test_real=True) - - assert info is not None - assert info.ip == "1.2.3.4" - assert info.country_code == "US" - assert info.country_name == "United States" - assert info.region_code == "CA" - assert info.region_name == "California" - assert info.city == "San Diego" - assert info.zip_code == "92122" - assert info.time_zone == "America/Los_Angeles" - assert info.latitude == 32.8594 - assert info.longitude == -117.2073 - assert not info.use_metric - - -async def test_detect_location_info_both_queries_fail(session): - """Ensure we return None if both queries fail.""" - with patch("homeassistant.util.location._get_ipapi", return_value=None), patch( - "homeassistant.util.location._get_ip_api", return_value=None - ): - info = await location_util.async_detect_location_info(session, _test_real=True) - assert info is None - - -async def test_freegeoip_query_raises(raising_session): - """Test ipapi.co query when the request to API fails.""" - info = await location_util._get_ipapi(raising_session) - assert info is None - - -async def test_ip_api_query_raises(raising_session): - """Test ip api query when the request to API fails.""" - info = await location_util._get_ip_api(raising_session) +async def test_whoami_query_raises(raising_session): + """Test whoami query when the request to API fails.""" + info = await location_util._get_whoami(raising_session) assert info is None