forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tractive integration (home-assistant#51002)
* Scaffold * Implement config flow * Add dymmy device tracker and TractiveClient * Add simple DeviceTracker * Add device info * Listen to tractive event and update tracker entities accordingly * Refactoring * Fix logging level * Handle connection errors * Remove sleep * Fix logging * Remove unused strings * Replace username config with email * Update aiotractive * Use debug instead of info * Cover config_flow * Update .coveragerc * Add quality scale to manifest * pylint * Update aiotractive * Do not emit SERVER_AVAILABLE, properly handle availability * Use async_get_clientsession Co-authored-by: Daniel Hjelseth Høyer <[email protected]> * Add @Danielhiversen as a codeowner * Remove the title from strings and translations * Update homeassistant/components/tractive/__init__.py Co-authored-by: Franck Nijhof <[email protected]> * Force CI * Use _attr style properties instead of methods * Remove entry_type * Remove quality scale * Make pyupgrade happy Co-authored-by: Daniel Hjelseth Høyer <[email protected]> Co-authored-by: Franck Nijhof <[email protected]>
- Loading branch information
1 parent
91ab86c
commit 25eb27c
Showing
14 changed files
with
525 additions
and
0 deletions.
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
Validating CODEOWNERS rules …
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
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,153 @@ | ||
"""The tractive integration.""" | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
import logging | ||
|
||
import aiotractive | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.dispatcher import async_dispatcher_send | ||
|
||
from .const import ( | ||
DOMAIN, | ||
RECONNECT_INTERVAL, | ||
SERVER_UNAVAILABLE, | ||
TRACKER_HARDWARE_STATUS_UPDATED, | ||
TRACKER_POSITION_UPDATED, | ||
) | ||
|
||
PLATFORMS = ["device_tracker"] | ||
|
||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up tractive from a config entry.""" | ||
data = entry.data | ||
|
||
hass.data.setdefault(DOMAIN, {}) | ||
|
||
client = aiotractive.Tractive( | ||
data[CONF_EMAIL], data[CONF_PASSWORD], session=async_get_clientsession(hass) | ||
) | ||
try: | ||
creds = await client.authenticate() | ||
except aiotractive.exceptions.TractiveError as error: | ||
await client.close() | ||
raise ConfigEntryNotReady from error | ||
|
||
tractive = TractiveClient(hass, client, creds["user_id"]) | ||
tractive.subscribe() | ||
|
||
hass.data[DOMAIN][entry.entry_id] = tractive | ||
|
||
hass.config_entries.async_setup_platforms(entry, PLATFORMS) | ||
|
||
async def cancel_listen_task(_): | ||
await tractive.unsubscribe() | ||
|
||
entry.async_on_unload( | ||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cancel_listen_task) | ||
) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
if unload_ok: | ||
tractive = hass.data[DOMAIN].pop(entry.entry_id) | ||
await tractive.unsubscribe() | ||
return unload_ok | ||
|
||
|
||
class TractiveClient: | ||
"""A Tractive client.""" | ||
|
||
def __init__(self, hass, client, user_id): | ||
"""Initialize the client.""" | ||
self._hass = hass | ||
self._client = client | ||
self._user_id = user_id | ||
self._listen_task = None | ||
|
||
@property | ||
def user_id(self): | ||
"""Return user id.""" | ||
return self._user_id | ||
|
||
async def trackable_objects(self): | ||
"""Get list of trackable objects.""" | ||
return await self._client.trackable_objects() | ||
|
||
def tracker(self, tracker_id): | ||
"""Get tracker by id.""" | ||
return self._client.tracker(tracker_id) | ||
|
||
def subscribe(self): | ||
"""Start event listener coroutine.""" | ||
self._listen_task = asyncio.create_task(self._listen()) | ||
|
||
async def unsubscribe(self): | ||
"""Stop event listener coroutine.""" | ||
if self._listen_task: | ||
self._listen_task.cancel() | ||
await self._client.close() | ||
|
||
async def _listen(self): | ||
server_was_unavailable = False | ||
while True: | ||
try: | ||
async for event in self._client.events(): | ||
if server_was_unavailable: | ||
_LOGGER.debug("Tractive is back online") | ||
server_was_unavailable = False | ||
if event["message"] != "tracker_status": | ||
continue | ||
|
||
if "hardware" in event: | ||
self._send_hardware_update(event) | ||
|
||
if "position" in event: | ||
self._send_position_update(event) | ||
except aiotractive.exceptions.TractiveError: | ||
_LOGGER.debug( | ||
"Tractive is not available. Internet connection is down? Sleeping %i seconds and retrying", | ||
RECONNECT_INTERVAL.total_seconds(), | ||
) | ||
async_dispatcher_send( | ||
self._hass, f"{SERVER_UNAVAILABLE}-{self._user_id}" | ||
) | ||
await asyncio.sleep(RECONNECT_INTERVAL.total_seconds()) | ||
server_was_unavailable = True | ||
continue | ||
|
||
def _send_hardware_update(self, event): | ||
payload = {"battery_level": event["hardware"]["battery_level"]} | ||
self._dispatch_tracker_event( | ||
TRACKER_HARDWARE_STATUS_UPDATED, event["tracker_id"], payload | ||
) | ||
|
||
def _send_position_update(self, event): | ||
payload = { | ||
"latitude": event["position"]["latlong"][0], | ||
"longitude": event["position"]["latlong"][1], | ||
"accuracy": event["position"]["accuracy"], | ||
} | ||
self._dispatch_tracker_event( | ||
TRACKER_POSITION_UPDATED, event["tracker_id"], payload | ||
) | ||
|
||
def _dispatch_tracker_event(self, event_name, tracker_id, payload): | ||
async_dispatcher_send( | ||
self._hass, | ||
f"{event_name}-{tracker_id}", | ||
payload, | ||
) |
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,74 @@ | ||
"""Config flow for tractive integration.""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import Any | ||
|
||
import aiotractive | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.exceptions import HomeAssistantError | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema({CONF_EMAIL: str, CONF_PASSWORD: str}) | ||
|
||
|
||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: | ||
"""Validate the user input allows us to connect. | ||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. | ||
""" | ||
|
||
client = aiotractive.api.API(data[CONF_EMAIL], data[CONF_PASSWORD]) | ||
try: | ||
user_id = await client.user_id() | ||
except aiotractive.exceptions.UnauthorizedError as error: | ||
raise InvalidAuth from error | ||
finally: | ||
await client.close() | ||
|
||
return {"title": data[CONF_EMAIL], "user_id": user_id} | ||
|
||
|
||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for tractive.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Handle the initial step.""" | ||
if user_input is None: | ||
return self.async_show_form( | ||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA | ||
) | ||
|
||
errors = {} | ||
|
||
try: | ||
info = await validate_input(self.hass, user_input) | ||
except InvalidAuth: | ||
errors["base"] = "invalid_auth" | ||
except Exception: # pylint: disable=broad-except | ||
_LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
else: | ||
await self.async_set_unique_id(info["user_id"]) | ||
self._abort_if_unique_id_configured() | ||
return self.async_create_entry(title=info["title"], data=user_input) | ||
|
||
return self.async_show_form( | ||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors | ||
) | ||
|
||
|
||
class InvalidAuth(HomeAssistantError): | ||
"""Error to indicate there is invalid auth.""" |
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,12 @@ | ||
"""Constants for the tractive integration.""" | ||
|
||
from datetime import timedelta | ||
|
||
DOMAIN = "tractive" | ||
|
||
RECONNECT_INTERVAL = timedelta(seconds=10) | ||
|
||
TRACKER_HARDWARE_STATUS_UPDATED = "tracker_hardware_status_updated" | ||
TRACKER_POSITION_UPDATED = "tracker_position_updated" | ||
|
||
SERVER_UNAVAILABLE = "tractive_server_unavailable" |
Oops, something went wrong.