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

Automatic odb device tracker #3035

Merged
merged 13 commits into from
Sep 3, 2016
24 changes: 18 additions & 6 deletions homeassistant/components/device_tracker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
ATTR_LOCATION_NAME = 'location_name'
ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery'
ATTR_ATTRIBUTES = 'attributes'

PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds
Expand Down Expand Up @@ -89,7 +90,7 @@ def is_on(hass: HomeAssistantType, entity_id: str=None):
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=None,
battery=None): # pylint: disable=too-many-arguments
battery=None, attrs: dict=None): # pylint: disable=too-many-arguments
Copy link
Member

Choose a reason for hiding this comment

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

Keep the name consistent attributes. Allows for passing as attributes={} in all see functions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks - addressed.

"""Call service to notify you see device."""
data = {key: value for key, value in
((ATTR_MAC, mac),
Expand All @@ -99,6 +100,9 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
(ATTR_GPS, gps),
(ATTR_GPS_ACCURACY, gps_accuracy),
(ATTR_BATTERY, battery)) if value is not None}
if attrs:
for key, value in attrs:
data[key] = value
hass.services.call(DOMAIN, SERVICE_SEE, data)


Expand Down Expand Up @@ -164,7 +168,7 @@ def see_service(call):
"""Service to see a device."""
args = {key: value for key, value in call.data.items() if key in
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
tracker.see(**args)

descriptions = load_yaml_config_file(
Expand Down Expand Up @@ -202,7 +206,7 @@ def __init__(self, hass: HomeAssistantType, consider_home: timedelta,

def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
battery: str=None):
battery: str=None, attributes: dict=None):
"""Notify the device tracker that you see a device."""
with self.lock:
if mac is None and dev_id is None:
Expand All @@ -218,7 +222,7 @@ def see(self, mac: str=None, dev_id: str=None, host_name: str=None,

if device:
device.seen(host_name, location_name, gps, gps_accuracy,
battery)
battery, attributes)
if device.track:
device.update_ha_state()
return
Expand All @@ -232,7 +236,8 @@ def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
if mac is not None:
self.mac_to_dev[mac] = device

device.seen(host_name, location_name, gps, gps_accuracy, battery)
device.seen(host_name, location_name, gps, gps_accuracy, battery,
attributes)
if device.track:
device.update_ha_state()

Expand Down Expand Up @@ -267,6 +272,7 @@ class Device(Entity):
gps_accuracy = 0
last_seen = None # type: dt_util.dt.datetime
battery = None # type: str
attributes = None # type: dict

# Track if the last update of this device was HOME.
last_update_home = False
Expand Down Expand Up @@ -330,6 +336,10 @@ def state_attributes(self):
if self.battery:
attr[ATTR_BATTERY] = self.battery

if self.attributes:
for key, value in self.attributes:
attr[key] = value

return attr

@property
Expand All @@ -338,13 +348,15 @@ def hidden(self):
return self.away_hide and self.state != STATE_HOME

def seen(self, host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=0, battery: str=None):
gps: GPSType=None, gps_accuracy=0, battery: str=None,
attributes: dict=None):
"""Mark the device as seen."""
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.gps_accuracy = gps_accuracy or 0
self.battery = battery
self.attributes = attributes
self.gps = None
if gps is not None:
try:
Expand Down
161 changes: 161 additions & 0 deletions homeassistant/components/device_tracker/automatic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""
Support for the Automatic platform.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.automatic/
"""
from datetime import timedelta
import logging
import re
import requests

import voluptuous as vol

from homeassistant.components.device_tracker import (PLATFORM_SCHEMA,
ATTR_ATTRIBUTES)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, datetime as dt_util

_LOGGER = logging.getLogger(__name__)

MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)

CONF_CLIENT_ID = 'client_id'
CONF_SECRET = 'secret'
CONF_DEVICES = 'devices'

SCOPE = 'scope:location scope:vehicle:profile scope:user:profile scope:trip'

ATTR_ACCESS_TOKEN = 'access_token'
ATTR_EXPIRES_IN = 'expires_in'
ATTR_RESULTS = 'results'
ATTR_VEHICLE = 'vehicle'
ATTR_ENDED_AT = 'ended_at'
ATTR_END_LOCATION = 'end_location'

URL_AUTHORIZE = 'https://accounts.automatic.com/oauth/access_token/'
URL_VEHICLES = 'https://api.automatic.com/vehicle/'
URL_TRIPS = 'https://api.automatic.com/trip/'

_VEHICLE_ID_REGEX = re.compile(
(URL_VEHICLES + '(.*)?[/]$').replace('/', r'\/'))

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_SECRET): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string])
})


def setup_scanner(hass, config: dict, see):
"""Validate the configuration and return an Automatic scanner."""
try:
AutomaticDeviceScanner(config, see)
except requests.HTTPError as err:
_LOGGER.error(str(err))
return False
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add except IOError as err: _LOGGER.error(str(err)) here to print your exception messages you throw below, else it will just fail silently


return True


class AutomaticDeviceScanner(object):
"""A class representing an Automatic device."""

def __init__(self, config: dict, see) -> None:
"""Initialize the automatic device scanner."""
self._devices = config.get(CONF_DEVICES, None)
self._access_token_payload = {
'username': config.get(CONF_USERNAME),
'password': config.get(CONF_PASSWORD),
'client_id': config.get(CONF_CLIENT_ID),
'client_secret': config.get(CONF_SECRET),
'grant_type': 'password',
'scope': SCOPE
}
Copy link
Member

Choose a reason for hiding this comment

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

👍

self._headers = None
self._token_expires = dt_util.now()
self.last_results = {}
self.last_trips = {}
self.see = see

self.scan_devices()

def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()

return [item['id'] for item in self.last_results]

def get_device_name(self, device):
"""Get the device name from id."""
vehicle = [item['display_name'] for item in self.last_results
if item['id'] == device]

return vehicle[0]

def _update_headers(self):
"""Get the access token from automatic."""
if self._headers is None or self._token_expires <= dt_util.now():
resp = requests.post(
URL_AUTHORIZE,
data=self._access_token_payload)

resp.raise_for_status()

json = resp.json()

access_token = json[ATTR_ACCESS_TOKEN]
self._token_expires = dt_util.now() + timedelta(
seconds=json[ATTR_EXPIRES_IN])
self._headers = {
'Authorization': 'Bearer {}'.format(access_token)
}

@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self) -> None:
"""Update the device info."""
_LOGGER.info('Updating devices')
self._update_headers()

response = requests.get(URL_VEHICLES, headers=self._headers)

response.raise_for_status()

self.last_results = [item for item in response.json()[ATTR_RESULTS]
if self._devices is None or item[
'display_name'] in self._devices]

response = requests.get(URL_TRIPS, headers=self._headers)

if response.status_code == 200:
for trip in response.json()[ATTR_RESULTS]:
vehicle_id = _VEHICLE_ID_REGEX.match(
trip[ATTR_VEHICLE]).group(1)
if vehicle_id not in self.last_trips:
self.last_trips[vehicle_id] = trip
elif self.last_trips[vehicle_id][ATTR_ENDED_AT] < trip[
ATTR_ENDED_AT]:
self.last_trips[vehicle_id] = trip

for vehicle in self.last_results:
dev_id = vehicle.get('id')

attrs = {
'fuel_level': vehicle.get('fuel_level_percent')
}

kwargs = {
'dev_id': dev_id,
'mac': dev_id,
ATTR_ATTRIBUTES: attrs
}

if dev_id in self.last_trips:
end_location = self.last_trips[dev_id][ATTR_END_LOCATION]
kwargs['gps'] = (end_location['lat'], end_location['lon'])
kwargs['gps_accuracy'] = end_location['accuracy_m']

self.see(**kwargs)
Loading