2016-01-26 23:08:06 +00:00
|
|
|
"""
|
|
|
|
Module with location helpers.
|
2016-02-05 06:26:02 +00:00
|
|
|
|
|
|
|
detect_location_info and elevation are mocked by default during tests.
|
2016-01-26 23:08:06 +00:00
|
|
|
"""
|
2021-03-17 20:46:07 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-05-08 18:15:04 +00:00
|
|
|
import asyncio
|
2015-07-07 07:01:17 +00:00
|
|
|
import collections
|
2016-05-29 18:55:16 +00:00
|
|
|
import math
|
2021-03-17 20:46:07 +00:00
|
|
|
from typing import Any
|
2016-07-28 03:33:49 +00:00
|
|
|
|
2019-05-08 18:15:04 +00:00
|
|
|
import aiohttp
|
2016-02-19 05:27:50 +00:00
|
|
|
|
2021-07-28 04:05:16 +00:00
|
|
|
from homeassistant.const import __version__ as HA_VERSION
|
|
|
|
|
2021-05-23 03:34:48 +00:00
|
|
|
WHOAMI_URL = "https://whoami.home-assistant.io/v1"
|
2021-07-28 04:05:16 +00:00
|
|
|
WHOAMI_URL_DEV = "https://whoami-v1-dev.home-assistant.workers.dev/v1"
|
2016-05-29 18:55:16 +00:00
|
|
|
|
|
|
|
# Constants from https://github.com/maurycyp/vincenty
|
|
|
|
# Earth ellipsoid according to WGS 84
|
|
|
|
# Axis a of the ellipsoid (Radius of the earth in meters)
|
|
|
|
AXIS_A = 6378137
|
|
|
|
# Flattening f = (a-b) / a
|
|
|
|
FLATTENING = 1 / 298.257223563
|
|
|
|
# Axis b of the ellipsoid in meters.
|
|
|
|
AXIS_B = 6356752.314245
|
|
|
|
|
|
|
|
MILES_PER_KILOMETER = 0.621371
|
|
|
|
MAX_ITERATIONS = 200
|
|
|
|
CONVERGENCE_THRESHOLD = 1e-12
|
2015-12-27 19:07:25 +00:00
|
|
|
|
2015-07-07 07:01:17 +00:00
|
|
|
LocationInfo = collections.namedtuple(
|
|
|
|
"LocationInfo",
|
2019-07-31 19:25:30 +00:00
|
|
|
[
|
|
|
|
"ip",
|
|
|
|
"country_code",
|
2021-07-28 04:05:16 +00:00
|
|
|
"currency",
|
2019-07-31 19:25:30 +00:00
|
|
|
"region_code",
|
|
|
|
"region_name",
|
|
|
|
"city",
|
|
|
|
"zip_code",
|
|
|
|
"time_zone",
|
|
|
|
"latitude",
|
|
|
|
"longitude",
|
|
|
|
"use_metric",
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_detect_location_info(
|
2019-10-29 06:32:34 +00:00
|
|
|
session: aiohttp.ClientSession,
|
2021-03-17 20:46:07 +00:00
|
|
|
) -> LocationInfo | None:
|
2016-02-27 22:58:36 +00:00
|
|
|
"""Detect location information."""
|
2021-05-23 03:34:48 +00:00
|
|
|
data = await _get_whoami(session)
|
2016-04-06 17:25:40 +00:00
|
|
|
|
2016-06-27 16:02:45 +00:00
|
|
|
if data is None:
|
2016-04-06 17:25:40 +00:00
|
|
|
return None
|
2015-07-07 07:01:17 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
data["use_metric"] = data["country_code"] not in ("US", "MM", "LR")
|
2015-07-07 07:01:17 +00:00
|
|
|
|
|
|
|
return LocationInfo(**data)
|
2015-09-20 16:35:03 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def distance(
|
2021-03-17 20:46:07 +00:00
|
|
|
lat1: float | None, lon1: float | None, lat2: float, lon2: float
|
|
|
|
) -> float | None:
|
2016-11-18 22:35:08 +00:00
|
|
|
"""Calculate the distance in meters between two points.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2018-07-17 22:28:44 +00:00
|
|
|
if lat1 is None or lon1 is None:
|
|
|
|
return None
|
|
|
|
result = vincenty((lat1, lon1), (lat2, lon2))
|
|
|
|
if result is None:
|
|
|
|
return None
|
|
|
|
return result * 1000
|
2015-12-27 19:07:25 +00:00
|
|
|
|
|
|
|
|
2016-05-29 18:55:16 +00:00
|
|
|
# Author: https://github.com/maurycyp
|
|
|
|
# Source: https://github.com/maurycyp/vincenty
|
|
|
|
# License: https://github.com/maurycyp/vincenty/blob/master/LICENSE
|
2019-07-31 19:25:30 +00:00
|
|
|
def vincenty(
|
2021-03-17 20:46:07 +00:00
|
|
|
point1: tuple[float, float], point2: tuple[float, float], miles: bool = False
|
|
|
|
) -> float | None:
|
2016-05-29 18:55:16 +00:00
|
|
|
"""
|
|
|
|
Vincenty formula (inverse method) to calculate the distance.
|
|
|
|
|
|
|
|
Result in kilometers or miles between two points on the surface of a
|
|
|
|
spheroid.
|
2016-11-18 22:35:08 +00:00
|
|
|
|
|
|
|
Async friendly.
|
2016-05-29 18:55:16 +00:00
|
|
|
"""
|
|
|
|
# short-circuit coincident points
|
|
|
|
if point1[0] == point2[0] and point1[1] == point2[1]:
|
|
|
|
return 0.0
|
|
|
|
|
2020-05-09 11:08:40 +00:00
|
|
|
# pylint: disable=invalid-name
|
2016-05-29 18:55:16 +00:00
|
|
|
U1 = math.atan((1 - FLATTENING) * math.tan(math.radians(point1[0])))
|
|
|
|
U2 = math.atan((1 - FLATTENING) * math.tan(math.radians(point2[0])))
|
|
|
|
L = math.radians(point2[1] - point1[1])
|
|
|
|
Lambda = L
|
|
|
|
|
|
|
|
sinU1 = math.sin(U1)
|
|
|
|
cosU1 = math.cos(U1)
|
|
|
|
sinU2 = math.sin(U2)
|
|
|
|
cosU2 = math.cos(U2)
|
|
|
|
|
2018-01-30 22:44:05 +00:00
|
|
|
for _ in range(MAX_ITERATIONS):
|
2016-05-29 18:55:16 +00:00
|
|
|
sinLambda = math.sin(Lambda)
|
|
|
|
cosLambda = math.cos(Lambda)
|
2019-07-31 19:25:30 +00:00
|
|
|
sinSigma = math.sqrt(
|
|
|
|
(cosU2 * sinLambda) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2
|
|
|
|
)
|
2019-05-02 18:18:20 +00:00
|
|
|
if sinSigma == 0.0:
|
2016-05-29 18:55:16 +00:00
|
|
|
return 0.0 # coincident points
|
|
|
|
cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda
|
|
|
|
sigma = math.atan2(sinSigma, cosSigma)
|
|
|
|
sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma
|
|
|
|
cosSqAlpha = 1 - sinAlpha ** 2
|
|
|
|
try:
|
|
|
|
cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha
|
|
|
|
except ZeroDivisionError:
|
|
|
|
cos2SigmaM = 0
|
2019-07-31 19:25:30 +00:00
|
|
|
C = FLATTENING / 16 * cosSqAlpha * (4 + FLATTENING * (4 - 3 * cosSqAlpha))
|
2016-05-29 18:55:16 +00:00
|
|
|
LambdaPrev = Lambda
|
2019-07-31 19:25:30 +00:00
|
|
|
Lambda = L + (1 - C) * FLATTENING * sinAlpha * (
|
|
|
|
sigma
|
|
|
|
+ C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2))
|
|
|
|
)
|
2016-05-29 18:55:16 +00:00
|
|
|
if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD:
|
|
|
|
break # successful convergence
|
|
|
|
else:
|
|
|
|
return None # failure to converge
|
|
|
|
|
|
|
|
uSq = cosSqAlpha * (AXIS_A ** 2 - AXIS_B ** 2) / (AXIS_B ** 2)
|
|
|
|
A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))
|
|
|
|
B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))
|
2019-07-31 19:25:30 +00:00
|
|
|
deltaSigma = (
|
|
|
|
B
|
|
|
|
* sinSigma
|
|
|
|
* (
|
|
|
|
cos2SigmaM
|
|
|
|
+ B
|
|
|
|
/ 4
|
|
|
|
* (
|
|
|
|
cosSigma * (-1 + 2 * cos2SigmaM ** 2)
|
|
|
|
- B
|
|
|
|
/ 6
|
|
|
|
* cos2SigmaM
|
|
|
|
* (-3 + 4 * sinSigma ** 2)
|
|
|
|
* (-3 + 4 * cos2SigmaM ** 2)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2016-05-29 18:55:16 +00:00
|
|
|
s = AXIS_B * A * (sigma - deltaSigma)
|
|
|
|
|
2017-09-23 15:15:46 +00:00
|
|
|
s /= 1000 # Conversion of meters to kilometers
|
2016-05-29 18:55:16 +00:00
|
|
|
if miles:
|
|
|
|
s *= MILES_PER_KILOMETER # kilometers to miles
|
|
|
|
|
2019-10-19 18:35:57 +00:00
|
|
|
return round(s, 6)
|
2016-06-27 16:02:45 +00:00
|
|
|
|
|
|
|
|
2021-05-23 03:34:48 +00:00
|
|
|
async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None:
|
|
|
|
"""Query whoami.home-assistant.io for location data."""
|
2016-06-27 16:02:45 +00:00
|
|
|
try:
|
2021-07-28 04:05:16 +00:00
|
|
|
resp = await session.get(
|
|
|
|
WHOAMI_URL_DEV if HA_VERSION.endswith("0.dev0") else WHOAMI_URL, timeout=30
|
|
|
|
)
|
2019-05-08 18:15:04 +00:00
|
|
|
except (aiohttp.ClientError, asyncio.TimeoutError):
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
raw_info = await resp.json()
|
|
|
|
except (aiohttp.ClientError, ValueError):
|
2016-06-27 16:02:45 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"ip": raw_info.get("ip"),
|
|
|
|
"country_code": raw_info.get("country"),
|
2021-07-28 04:05:16 +00:00
|
|
|
"currency": raw_info.get("currency"),
|
2019-07-31 19:25:30 +00:00
|
|
|
"region_code": raw_info.get("region_code"),
|
|
|
|
"region_name": raw_info.get("region"),
|
|
|
|
"city": raw_info.get("city"),
|
2021-05-23 03:34:48 +00:00
|
|
|
"zip_code": raw_info.get("postal_code"),
|
2019-07-31 19:25:30 +00:00
|
|
|
"time_zone": raw_info.get("timezone"),
|
2021-05-23 03:34:48 +00:00
|
|
|
"latitude": float(raw_info.get("latitude")),
|
|
|
|
"longitude": float(raw_info.get("longitude")),
|
2016-06-27 16:02:45 +00:00
|
|
|
}
|