-
-
Notifications
You must be signed in to change notification settings - Fork 29.9k
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
MartinHjelmare
merged 33 commits into
home-assistant:dev
from
exxamalte:geo_location_component
Aug 30, 2018
Merged
Geo Location component #15953
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 608ba1a
ensure that custom attributes don't override built-in ones
exxamalte 74f3e03
bugfixes and tests
exxamalte c110a7f
fixing tests because of introduction of new component using same fixture
exxamalte d58f258
improving test cases
exxamalte d726c33
removing potentially unavailable attribute from debug message output
exxamalte b3eed04
completing test suite
exxamalte b021137
cleaning up debug messages; sorting entries in group view by distance
exxamalte 771dee7
ability to define the desired state attribute and corresponding unit …
exxamalte 4d1b109
sort entries in group; code clean-ups
exxamalte c258283
fixing indentation
exxamalte 6b69d28
added requirements of new component and platform
exxamalte cb32386
fixed various lint issues
exxamalte 645916d
fixed more lint issues
exxamalte 0b45966
introducing demo geo location platform; refactored geo location compo…
exxamalte a3f782f
removing geo rss events platform; added unit tests for geo location p…
exxamalte d460ebe
reverting change in debug message for feedreader to avoid confusion w…
exxamalte 57e62df
updated requirements after removing georss platform
exxamalte 5629c3f
removed unused imports
exxamalte 6697147
fixing a lint issue and a test case
exxamalte 36b451b
simplifying component code; moving code into demo platform; fixing tests
exxamalte 693cba6
removed grouping from demo platform; small refactorings
exxamalte ff1a9e5
automating the entity id generation (the use of an entity namespace a…
exxamalte 3ae19c4
undoing changes made for the georss platform
exxamalte 99780b8
simplified test cases
exxamalte e8e1d52
small tweaks to test case
exxamalte c26ead9
rounding all state attribute values
exxamalte 05f274d
fixing lint; removing distance from state attributes
exxamalte 7d779f9
fixed test
exxamalte 19be535
renamed add_devices to add_entities; tweaked test to gain more contro…
exxamalte 9467e84
reusing utcnow variable instead of patched method
exxamalte fd063e4
fixed test by avoiding to make assumptions about order of list of ent…
exxamalte cd0fe02
adding test for the geo location event class
exxamalte File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""The tests for Geo Location platforms.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() + | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK