Skip to content

Commit 36aa941

Browse files
committed
feat: map for entities and multiple map services
Why: * openstreetmap isn't always the best option. * there are more than just configuration with location info. This change addreses the need by: * introduce a `--service` option to let you choose which map service to use. currently: openstreetmap, google and bing implemented * add optional entity parameter to lookup and it has latitiude and longitude in its state it will be mapped. * bing has extra feature of allowing showing a name/label, here `friendly_name` is used otherwise `entity_id` Example usage: `hass-cli map person.max` `hass-cli --service bing device_tracker.maxs_phone` Future idea: allow to map multiple points, can be done as directions in google but Bing has better support.
1 parent 498d8ea commit 36aa941

File tree

2 files changed

+106
-10
lines changed

2 files changed

+106
-10
lines changed

homeassistant_cli/plugins/map.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,68 @@
11
"""Map plugin for Home Assistant CLI (hass-cli)."""
2+
import sys
23
import webbrowser
34

45
import click
6+
import homeassistant_cli.autocompletion as autocompletion
57
from homeassistant_cli.cli import pass_context
68
from homeassistant_cli.config import Configuration
79
import homeassistant_cli.remote as api
810

9-
OSM_URL = 'https://www.openstreetmap.org'
10-
ZOOM = 17
11+
OSM_URL = "https://www.openstreetmap.org/"
12+
GOOGLE_URL = "https://www.google.com/maps/search/"
13+
BING_URL = "https://www.bing.com/maps"
14+
SERVICE = {
15+
'openstreetmap': OSM_URL + '?mlat={0}&mlon={1}#map=17/{0}/{1}',
16+
'google': GOOGLE_URL + '?api=1&query={0},{1}',
17+
'bing': BING_URL + '?v=2&cp={0}~{1}&lvl=17&sp=point.{0}_{1}_{2}',
18+
}
1119

1220

1321
@click.command('map')
22+
@click.argument( # type: ignore
23+
'entity', required=False, autocompletion=autocompletion.entities
24+
)
25+
@click.option(
26+
'--service', default='openstreetmap', type=click.Choice(SERVICE.keys())
27+
)
1428
@pass_context
15-
def cli(ctx: Configuration) -> None:
16-
"""Print the current location on a map."""
17-
response = api.get_config(ctx)
29+
def cli(ctx: Configuration, service: str, entity: str) -> None:
30+
"""Show the location of the config or an entity on a map."""
31+
latitude = None
32+
longitude = None
1833

19-
if response:
20-
url = '{0}/?mlat={2}&mlon={3}#map={1}/{2}/{3}'.format(
21-
OSM_URL, ZOOM, response.get('latitude'), response.get('longitude')
22-
)
23-
webbrowser.open_new_tab(url)
34+
if entity:
35+
thing = entity
36+
data = api.get_state(ctx, entity)
37+
if data:
38+
attr = data.get('attributes', {})
39+
latitude = attr.get('latitude')
40+
longitude = attr.get('longitude')
41+
thing = attr.get('friendly_name', entity)
42+
else:
43+
thing = "configuration"
44+
response = api.get_config(ctx)
45+
if response:
46+
latitude = response.get('latitude')
47+
longitude = response.get('longitude')
48+
thing = response.get('location_name', thing)
49+
50+
if latitude and longitude:
51+
urlpattern = SERVICE.get(service)
52+
import urllib.parse
53+
54+
if urlpattern:
55+
url = urlpattern.format(
56+
latitude, longitude, urllib.parse.quote_plus(thing)
57+
)
58+
ctx.echo(
59+
"{} location is at {}, {}".format(thing, latitude, longitude)
60+
)
61+
webbrowser.open_new_tab(url)
62+
else:
63+
ctx.echo(
64+
"Could not find url pattern for service {}".format(service)
65+
)
66+
else:
67+
ctx.echo("No exact location info found in {}".format(thing))
68+
sys.exit(2)

tests/test_map.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tests file for hass-cli map."""
2+
from typing import no_type_check
3+
from unittest.mock import patch
4+
5+
from click.testing import CliRunner
6+
import homeassistant_cli.cli as cli
7+
import pytest
8+
import requests_mock
9+
10+
11+
@no_type_check
12+
@pytest.mark.parametrize(
13+
"service,url",
14+
[
15+
("openstreetmap", "https://www.openstreetmap.org"),
16+
("bing", "https://www.bing.com"),
17+
("google", "https://www.google.com"),
18+
],
19+
)
20+
def test_map_services(service, url, default_entities) -> None:
21+
"""Test map feature."""
22+
entity_id = 'zone.school'
23+
school = next(
24+
(x for x in default_entities if x['entity_id'] == entity_id), "ERROR!"
25+
)
26+
27+
print(school)
28+
with requests_mock.Mocker() as mock, patch(
29+
'webbrowser.open_new_tab'
30+
) as mocked_browser:
31+
mock.get(
32+
"http://localhost:8123/api/states/{}".format(entity_id),
33+
json=school,
34+
status_code=200,
35+
)
36+
37+
runner = CliRunner()
38+
result = runner.invoke(
39+
cli.cli,
40+
["--output=json", "map", "--service", service, "zone.school"],
41+
catch_exceptions=False,
42+
)
43+
assert result.exit_code == 0
44+
45+
callurl = mocked_browser.call_args[0][0]
46+
47+
assert callurl.startswith(url)
48+
assert str(school.get('attributes').get('latitude')) in callurl
49+
assert (
50+
str(school.get('attributes').get('longitude')) in callurl
51+
) # typing: ignore

0 commit comments

Comments
 (0)