Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Geo Location component #15953

Merged
merged 33 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7f854c8
initial working version of a geo location component and georss platform
exxamalte Jul 26, 2018
608ba1a
ensure that custom attributes don't override built-in ones
exxamalte Jul 27, 2018
74f3e03
bugfixes and tests
exxamalte Aug 4, 2018
c110a7f
fixing tests because of introduction of new component using same fixture
exxamalte Aug 4, 2018
d58f258
improving test cases
exxamalte Aug 6, 2018
d726c33
removing potentially unavailable attribute from debug message output
exxamalte Aug 6, 2018
b3eed04
completing test suite
exxamalte Aug 6, 2018
b021137
cleaning up debug messages; sorting entries in group view by distance
exxamalte Aug 7, 2018
771dee7
ability to define the desired state attribute and corresponding unit …
exxamalte Aug 9, 2018
4d1b109
sort entries in group; code clean-ups
exxamalte Aug 10, 2018
c258283
fixing indentation
exxamalte Aug 10, 2018
6b69d28
added requirements of new component and platform
exxamalte Aug 13, 2018
cb32386
fixed various lint issues
exxamalte Aug 13, 2018
645916d
fixed more lint issues
exxamalte Aug 13, 2018
0b45966
introducing demo geo location platform; refactored geo location compo…
exxamalte Aug 16, 2018
a3f782f
removing geo rss events platform; added unit tests for geo location p…
exxamalte Aug 20, 2018
d460ebe
reverting change in debug message for feedreader to avoid confusion w…
exxamalte Aug 20, 2018
57e62df
updated requirements after removing georss platform
exxamalte Aug 20, 2018
5629c3f
removed unused imports
exxamalte Aug 20, 2018
6697147
fixing a lint issue and a test case
exxamalte Aug 21, 2018
36b451b
simplifying component code; moving code into demo platform; fixing tests
exxamalte Aug 21, 2018
693cba6
removed grouping from demo platform; small refactorings
exxamalte Aug 21, 2018
ff1a9e5
automating the entity id generation (the use of an entity namespace a…
exxamalte Aug 22, 2018
3ae19c4
undoing changes made for the georss platform
exxamalte Aug 22, 2018
99780b8
simplified test cases
exxamalte Aug 22, 2018
e8e1d52
small tweaks to test case
exxamalte Aug 22, 2018
c26ead9
rounding all state attribute values
exxamalte Aug 28, 2018
05f274d
fixing lint; removing distance from state attributes
exxamalte Aug 28, 2018
7d779f9
fixed test
exxamalte Aug 28, 2018
19be535
renamed add_devices to add_entities; tweaked test to gain more contro…
exxamalte Aug 28, 2018
9467e84
reusing utcnow variable instead of patched method
exxamalte Aug 29, 2018
fd063e4
fixed test by avoiding to make assumptions about order of list of ent…
exxamalte Aug 29, 2018
cd0fe02
adding test for the geo location event class
exxamalte Aug 30, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions homeassistant/components/geo_location/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Geo Location component.

This component covers platforms that deal with external events that contain
a geo location related to the installed HA instance.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/geo_location/
"""
import logging
from datetime import timedelta
from typing import Optional

from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent

_LOGGER = logging.getLogger(__name__)

ATTR_DISTANCE = 'distance'
DOMAIN = 'geo_location'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
GROUP_NAME_ALL_EVENTS = 'All Geo Location Events'
SCAN_INTERVAL = timedelta(seconds=60)


async def async_setup(hass, config):
"""Set up this component."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS)
await component.async_setup(config)
return True


class GeoLocationEvent(Entity):
"""This represents an external event with an associated geo location."""

@property
def state(self):
"""Return the state of the sensor."""
if self.distance is not None:
return round(self.distance, 1)
return None

@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return None

@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return None

@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return None

@property
def state_attributes(self):
"""Return the state attributes of this external event."""
data = {}
if self.latitude is not None:
data[ATTR_LATITUDE] = round(self.latitude, 5)
if self.longitude is not None:
data[ATTR_LONGITUDE] = round(self.longitude, 5)
return data
132 changes: 132 additions & 0 deletions homeassistant/components/geo_location/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""
Demo platform for the geo location component.

For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import logging
import random
from datetime import timedelta
from math import pi, cos, sin, radians

from typing import Optional

from homeassistant.components.geo_location import GeoLocationEvent
from homeassistant.helpers.event import track_time_interval

_LOGGER = logging.getLogger(__name__)

AVG_KM_PER_DEGREE = 111.0
DEFAULT_UNIT_OF_MEASUREMENT = "km"
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
MAX_RADIUS_IN_KM = 50
NUMBER_OF_DEMO_DEVICES = 5

EVENT_NAMES = ["Bushfire", "Hazard Reduction", "Grass Fire", "Burn off",
"Structure Fire", "Fire Alarm", "Thunderstorm", "Tornado",
"Cyclone", "Waterspout", "Dust Storm", "Blizzard", "Ice Storm",
"Earthquake", "Tsunami"]


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Demo geo locations."""
DemoManager(hass, add_entities)


class DemoManager:
"""Device manager for demo geo location events."""

def __init__(self, hass, add_entities):
"""Initialise the demo geo location event manager."""
self._hass = hass
self._add_entities = add_entities
self._managed_devices = []
self._update(count=NUMBER_OF_DEMO_DEVICES)
self._init_regular_updates()

def _generate_random_event(self):
"""Generate a random event in vicinity of this HA instance."""
home_latitude = self._hass.config.latitude
home_longitude = self._hass.config.longitude

# Approx. 111km per degree (north-south).
radius_in_degrees = random.random() * MAX_RADIUS_IN_KM / \
AVG_KM_PER_DEGREE
radius_in_km = radius_in_degrees * AVG_KM_PER_DEGREE
angle = random.random() * 2 * pi
# Compute coordinates based on radius and angle. Adjust longitude value
# based on HA's latitude.
latitude = home_latitude + radius_in_degrees * sin(angle)
longitude = home_longitude + radius_in_degrees * cos(angle) / \
cos(radians(home_latitude))

event_name = random.choice(EVENT_NAMES)
return DemoGeoLocationEvent(event_name, radius_in_km, latitude,
longitude, DEFAULT_UNIT_OF_MEASUREMENT)

def _init_regular_updates(self):
"""Schedule regular updates based on configured time interval."""
track_time_interval(self._hass, lambda now: self._update(),
DEFAULT_UPDATE_INTERVAL)

def _update(self, count=1):
"""Remove events and add new random events."""
# Remove devices.
for _ in range(1, count + 1):
if self._managed_devices:
device = random.choice(self._managed_devices)
if device:
_LOGGER.debug("Removing %s", device)
self._managed_devices.remove(device)
self._hass.add_job(device.async_remove())
# Generate new devices from events.
new_devices = []
for _ in range(1, count + 1):
new_device = self._generate_random_event()
_LOGGER.debug("Adding %s", new_device)
new_devices.append(new_device)
self._managed_devices.append(new_device)
self._add_entities(new_devices)


class DemoGeoLocationEvent(GeoLocationEvent):
"""This represents a demo geo location event."""

def __init__(self, name, distance, latitude, longitude,
unit_of_measurement):
"""Initialize entity with data provided."""
self._name = name
self._distance = distance
self._latitude = latitude
self._longitude = longitude
self._unit_of_measurement = unit_of_measurement

@property
def name(self) -> Optional[str]:
"""Return the name of the event."""
return self._name

@property
def should_poll(self):
"""No polling needed for a demo geo location event."""
return False

@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return self._distance

@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return self._latitude

@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return self._longitude

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
1 change: 1 addition & 0 deletions tests/components/geo_location/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The tests for Geo Location platforms."""
62 changes: 62 additions & 0 deletions tests/components/geo_location/test_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""The tests for the demo platform."""
import unittest
from unittest.mock import patch

from homeassistant.components import geo_location
from homeassistant.components.geo_location.demo import \
NUMBER_OF_DEMO_DEVICES, DEFAULT_UNIT_OF_MEASUREMENT, \
DEFAULT_UPDATE_INTERVAL
from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, assert_setup_component, \
fire_time_changed
import homeassistant.util.dt as dt_util

CONFIG = {
geo_location.DOMAIN: [
{
'platform': 'demo'
}
]
}


class TestDemoPlatform(unittest.TestCase):
"""Test the demo platform."""

def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()

def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()

def test_setup_platform(self):
"""Test setup of demo platform via configuration."""
utcnow = dt_util.utcnow()
# Patching 'utcnow' to gain more control over the timed update.
with patch('homeassistant.util.dt.utcnow', return_value=utcnow):
with assert_setup_component(1, geo_location.DOMAIN):
self.assertTrue(setup_component(self.hass, geo_location.DOMAIN,
CONFIG))
entity_ids = self.hass.states.entity_ids(geo_location.DOMAIN)
assert len(entity_ids) == NUMBER_OF_DEMO_DEVICES
state_first_entry = self.hass.states.get(entity_ids[0])
state_last_entry = self.hass.states.get(entity_ids[-1])
# Check a single device's attributes.
self.assertAlmostEqual(state_first_entry.attributes['latitude'],
self.hass.config.latitude, delta=1.0)
self.assertAlmostEqual(state_first_entry.attributes['longitude'],
self.hass.config.longitude, delta=1.0)
assert state_first_entry.attributes['unit_of_measurement'] == \
DEFAULT_UNIT_OF_MEASUREMENT
# Update (replaces 1 device).
fire_time_changed(self.hass, dt_util.utcnow() +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the cached time value here utcnow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

DEFAULT_UPDATE_INTERVAL)
self.hass.block_till_done()
entity_ids_updated = self.hass.states.entity_ids(
geo_location.DOMAIN)
states_last_entry_updated = self.hass.states.get(
entity_ids_updated[-1])
# New entry was added to the end of the end of the array.
assert state_last_entry is not states_last_entry_updated
9 changes: 9 additions & 0 deletions tests/components/geo_location/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""The tests for the geo location component."""
from homeassistant.components import geo_location
from homeassistant.setup import async_setup_component


async def test_setup_component(hass):
"""Simple test setup of component."""
result = await async_setup_component(hass, geo_location.DOMAIN)
assert result