An async Python client library for the LoJack API, designed for Home Assistant integrations.
Project description
lojack_api
An async Python client library for the Spireon LoJack API, designed for Home Assistant integrations.
Features
- Async-first design - Built with
asyncioandaiohttpfor non-blocking I/O - No httpx dependency - Uses
aiohttpto avoid version conflicts with Home Assistant - Spireon LoJack API - Full support for the Spireon identity and services APIs
- Session management - Automatic token refresh and session resumption support
- Type hints - Full typing support with
py.typedmarker - Clean device abstractions - Device and Vehicle wrappers with convenient methods
Installation
# From the repository
pip install .
# With development dependencies
pip install .[dev]
Quick Start
Basic Usage
import asyncio
from lojack_api import LoJackClient
async def main():
# Create and authenticate (uses default Spireon URLs)
async with await LoJackClient.create(
"your_username",
"your_password"
) as client:
# List all devices/vehicles
devices = await client.list_devices()
for device in devices:
print(f"Device: {device.name} ({device.id})")
# Get current location
location = await device.get_location()
if location:
print(f" Location: {location.latitude}, {location.longitude}")
asyncio.run(main())
Session Resumption (for Home Assistant)
For Home Assistant integrations, you can persist authentication across restarts:
from lojack_api import LoJackClient, AuthArtifacts
# First time - login and save auth
async def initial_login(username, password):
client = await LoJackClient.create(username, password)
auth_data = client.export_auth().to_dict()
# Save auth_data to Home Assistant storage
await client.close()
return auth_data
# Later - resume without re-entering password
async def resume_session(auth_data, username=None, password=None):
auth = AuthArtifacts.from_dict(auth_data)
# Pass credentials for auto-refresh if token expires
client = await LoJackClient.from_auth(auth, username=username, password=password)
return client
Using External aiohttp Session
For Home Assistant integrations, pass the shared session:
from aiohttp import ClientSession
from lojack_api import LoJackClient
async def setup(hass_session: ClientSession, username, password):
client = await LoJackClient.create(
username,
password,
session=hass_session # Won't be closed when client closes
)
return client
Working with Vehicles
Vehicles have additional properties:
from lojack_api import Vehicle
async def vehicle_example(client):
devices = await client.list_devices()
for device in devices:
if isinstance(device, Vehicle):
print(f"Vehicle: {device.name}")
print(f" VIN: {device.vin}")
print(f" Make: {device.make} {device.model} ({device.year})")
Requesting Fresh Location Data
The Spireon REST API may return stale location data (30-76+ minutes old) because devices report periodically, not continuously. Two methods are available to request on-demand location updates:
Method Comparison
| Method | Returns | Use Case |
|---|---|---|
request_location_update() |
bool |
Fire-and-forget; scripts, simple polling |
request_fresh_location() |
datetime | None |
Non-blocking with baseline; Home Assistant |
request_location_update() -> bool
Sends a "locate" command to the device. Returns True if the command was
accepted by the API. This is a fire-and-forget method - you must poll
separately to detect when fresh data arrives.
# Simple usage - send command and poll manually
success = await device.request_location_update()
if success:
await asyncio.sleep(30) # Wait for device to respond
location = await device.get_location(force=True)
request_fresh_location() -> datetime | None
Sends a "locate" command and returns the current location timestamp as a baseline for comparison. This is the recommended method for Home Assistant integrations because it's non-blocking and provides a reference point to detect when fresh data arrives.
from datetime import datetime, timezone
# In a service call or button handler
baseline_ts = await device.request_fresh_location()
# Later, in your DataUpdateCoordinator's _async_update_data:
location = await device.get_location(force=True)
if location and location.timestamp:
# Check if we received fresh data since the locate command
if baseline_ts and location.timestamp > baseline_ts:
print("Fresh location received!")
age = (datetime.now(timezone.utc) - location.timestamp).total_seconds()
Returns:
datetime- The location timestamp before the locate command was sentNone- If no prior location was available
Location History
# Get location history
async for location in device.get_history(limit=100):
print(f"{location.timestamp}: {location.latitude}, {location.longitude}")
Troubleshooting Script
For debugging location freshness issues:
# Show current location ages
python scripts/poll_locations.py
# Request fresh location and monitor for updates
python scripts/poll_locations.py --locate
# Poll continuously every 30 seconds
python scripts/poll_locations.py --poll 30
Geofences
Geofences define circular areas that can trigger alerts when a device enters or exits the boundary.
# List all geofences for a device
geofences = await device.list_geofences()
for geofence in geofences:
print(f"{geofence.name}: {geofence.latitude}, {geofence.longitude} (r={geofence.radius}m)")
# Create a geofence
geofence = await device.create_geofence(
name="Home",
latitude=32.8427,
longitude=-97.0715,
radius=100.0, # meters
address="123 Main St"
)
# Update a geofence
await device.update_geofence(
geofence.id,
name="Home Base",
radius=150.0
)
# Delete a geofence
await device.delete_geofence(geofence.id)
Updating Device Information
Update device/vehicle metadata:
# Update device name
await device.update(name="My Tracker")
# For vehicles, update additional fields
await vehicle.update(
name="Family Car",
odometer=51000.0,
color="Blue"
)
Maintenance Schedules (Vehicles)
Get maintenance schedule information for vehicles with a VIN:
# Get maintenance schedule
schedule = await vehicle.get_maintenance_schedule()
if schedule:
print(f"Maintenance items for VIN {schedule.vin}:")
for item in schedule.items:
print(f" {item.name}: {item.severity}")
if item.mileage_due:
print(f" Due at: {item.mileage_due} miles")
if item.action:
print(f" Action: {item.action}")
Repair Orders (Vehicles)
Get repair order history for vehicles:
# Get repair orders
orders = await vehicle.get_repair_orders()
for order in orders:
print(f"Order {order.id}: {order.status}")
if order.description:
print(f" Description: {order.description}")
if order.total_amount:
print(f" Total: ${order.total_amount:.2f}")
if order.open_date:
print(f" Opened: {order.open_date.isoformat()}")
User Information
Get information about the authenticated user and accounts:
# Get user info
user_info = await client.get_user_info()
if user_info:
print(f"User: {user_info.get('email')}")
# Get accounts
accounts = await client.get_accounts()
print(f"Found {len(accounts)} account(s)")
API Reference
LoJackClient
The main entry point for the API.
# Factory methods (using default Spireon URLs)
client = await LoJackClient.create(username, password)
client = await LoJackClient.from_auth(auth_artifacts)
# With custom URLs
client = await LoJackClient.create(
username,
password,
identity_url="https://identity.spireon.com",
services_url="https://services.spireon.com/v0/rest"
)
# Properties
client.is_authenticated # bool
client.user_id # Optional[str]
# Methods
devices = await client.list_devices() # List[Device | Vehicle]
device = await client.get_device(device_id) # Device | Vehicle
locations = await client.get_locations(device_id, limit=10)
success = await client.send_command(device_id, "locate")
auth = client.export_auth() # AuthArtifacts
await client.close()
Device
Wrapper for tracked devices.
# Properties
device.id # str
device.name # Optional[str]
device.info # DeviceInfo
device.last_seen # Optional[datetime]
device.cached_location # Optional[Location]
# Methods
await device.refresh(force=True) # Refresh cached location from API
location = await device.get_location(force=False) # Get location (from cache or API)
async for loc in device.get_history(limit=100): # Iterate location history
...
# Location update methods
success = await device.request_location_update() # bool - fire-and-forget locate
baseline = await device.request_fresh_location() # datetime|None - locate + baseline
# Device updates
await device.update(name="New Name") # Update device info
# Geofence management
geofences = await device.list_geofences() # List[Geofence]
geofence = await device.get_geofence(geofence_id) # Geofence|None
geofence = await device.create_geofence(name=..., latitude=..., longitude=..., radius=...)
await device.update_geofence(geofence_id, name=..., radius=...)
await device.delete_geofence(geofence_id)
# Properties
device.location_timestamp # datetime|None - cached location's timestamp
Vehicle (extends Device)
Additional properties and methods for vehicles.
# Properties
vehicle.vin # Optional[str]
vehicle.make # Optional[str]
vehicle.model # Optional[str]
vehicle.year # Optional[int]
vehicle.license_plate # Optional[str]
vehicle.odometer # Optional[float]
# Methods (extends Device methods)
await vehicle.update(name=..., make=..., model=..., year=..., vin=..., odometer=...)
# Maintenance and repair methods
schedule = await vehicle.get_maintenance_schedule() # MaintenanceSchedule|None
orders = await vehicle.get_repair_orders() # List[RepairOrder]
Data Models
from lojack_api import Location, DeviceInfo, VehicleInfo
# Location - Core fields
location.latitude # Optional[float]
location.longitude # Optional[float]
location.timestamp # Optional[datetime]
location.accuracy # Optional[float] - GPS accuracy in METERS (for HA gps_accuracy)
location.speed # Optional[float]
location.heading # Optional[float]
location.address # Optional[str]
# Note on accuracy: The API may return HDOP values or quality strings.
# These are automatically converted to meters for Home Assistant compatibility:
# - HDOP values (1-15) are multiplied by 5 to get approximate meters
# - Quality strings ("GOOD", "POOR", etc.) are mapped to reasonable meter values
# - Values > 15 are assumed to already be in meters
# Location - Extended telemetry (from events)
location.odometer # Optional[float] - Vehicle odometer reading
location.battery_voltage # Optional[float] - Battery voltage
location.engine_hours # Optional[float] - Engine hours
location.distance_driven # Optional[float] - Total distance driven
location.signal_strength # Optional[float] - Signal strength (0.0 to 1.0)
location.gps_fix_quality # Optional[str] - GPS quality (e.g., "GOOD", "POOR")
location.event_type # Optional[str] - Event type (e.g., "SLEEP_ENTER")
location.event_id # Optional[str] - Unique event identifier
# Location - Raw data
location.raw # Dict[str, Any] - Original API response
# Geofence
from lojack_api import Geofence
geofence.id # str - Unique identifier
geofence.name # Optional[str] - Display name
geofence.latitude # Optional[float] - Center latitude
geofence.longitude # Optional[float] - Center longitude
geofence.radius # Optional[float] - Radius in meters
geofence.address # Optional[str] - Address description
geofence.active # bool - Whether geofence is active
geofence.asset_id # Optional[str] - Associated device ID
geofence.raw # Dict[str, Any] - Original API response
# Maintenance models
from lojack_api import MaintenanceItem, MaintenanceSchedule
# MaintenanceItem - Single service item
item.name # str - Service name (e.g., "Oil Change")
item.description # Optional[str] - Detailed description
item.severity # Optional[str] - Severity level ("NORMAL", "WARNING", "CRITICAL")
item.mileage_due # Optional[float] - Mileage at which service is due
item.months_due # Optional[int] - Months until service is due
item.action # Optional[str] - Recommended action
item.raw # Dict[str, Any] - Original API response
# MaintenanceSchedule - Collection of maintenance items
schedule.vin # str - Vehicle VIN
schedule.items # List[MaintenanceItem] - Scheduled services
schedule.raw # Dict[str, Any] - Original API response
# RepairOrder - Service/repair record
from lojack_api import RepairOrder
order.id # str - Unique repair order identifier
order.vin # Optional[str] - Vehicle VIN
order.asset_id # Optional[str] - Associated asset ID
order.status # Optional[str] - Order status ("OPEN", "CLOSED")
order.open_date # Optional[datetime] - When order was opened
order.close_date # Optional[datetime] - When order was closed
order.description # Optional[str] - Description of repair
order.total_amount # Optional[float] - Total cost
order.raw # Dict[str, Any] - Original API response
Exceptions
from lojack_api import (
LoJackError, # Base exception
AuthenticationError, # 401 errors, invalid credentials
AuthorizationError, # 403 errors, permission denied
ApiError, # Other API errors (has status_code)
ConnectionError, # Network connectivity issues
TimeoutError, # Request timeouts
DeviceNotFoundError, # Device not found (has device_id)
CommandError, # Command failed (has command, device_id)
)
Spireon API Details
The library uses the Spireon LoJack API:
- Identity Service:
https://identity.spireon.com- For authentication - Services API:
https://services.spireon.com/v0/rest- For device/asset management
Authentication uses HTTP Basic Auth with the following headers:
X-Nspire-Apptoken- Application tokenX-Nspire-Correlationid- Unique request IDX-Nspire-Usertoken- User token (after authentication)
Development
# Install dev dependencies
pip install .[dev]
# Run tests
pytest
# Run tests with coverage
pytest --cov=lojack_api
# Type checking
mypy lojack_api
# Linting
# Preferred: ruff for quick fixes
ruff check .
# Use flake8 for strict style checks (reports shown in CI)
# Match ruff's line length setting
flake8 lojack_api/ tests/ --count --show-source --statistics --max-line-length=100
License
MIT License - see LICENSE for details.
Contributing
Contributions are welcome! This library is designed to be vendored into Home Assistant integrations to avoid dependency conflicts.
Credits
This library was inspired by the original lojack-clients package and uses the Spireon LoJack API endpoints.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lojack_api-0.7.1.tar.gz.
File metadata
- Download URL: lojack_api-0.7.1.tar.gz
- Upload date:
- Size: 48.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67835a2d54d44ac30c4e7d928f402a42756dc115f7f02cfdbc6234f4d6b9da82
|
|
| MD5 |
2ec9ef17bdb1c9eda2fd4be6b983a597
|
|
| BLAKE2b-256 |
7cc2c8db60498ad0800b32eddc7c65b8b6a97e1ddf1363595e1b2ab52c327893
|
Provenance
The following attestation bundles were made for lojack_api-0.7.1.tar.gz:
Publisher:
publish.yml on devinslick/lojack_api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lojack_api-0.7.1.tar.gz -
Subject digest:
67835a2d54d44ac30c4e7d928f402a42756dc115f7f02cfdbc6234f4d6b9da82 - Sigstore transparency entry: 927296375
- Sigstore integration time:
-
Permalink:
devinslick/lojack_api@2a15e420a3bb8ca3dc4f5a80d1b23d4b35f5d591 -
Branch / Tag:
refs/tags/0.7.1 - Owner: https://github.com/devinslick
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2a15e420a3bb8ca3dc4f5a80d1b23d4b35f5d591 -
Trigger Event:
release
-
Statement type:
File details
Details for the file lojack_api-0.7.1-py3-none-any.whl.
File metadata
- Download URL: lojack_api-0.7.1-py3-none-any.whl
- Upload date:
- Size: 30.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a492f534b8f86c6b8a66c572cd4083233ab34342093d9962e7a9dee1bdc330cd
|
|
| MD5 |
04970cb7e6491efdb0fa19774bcc9aac
|
|
| BLAKE2b-256 |
e3a5e49e1f845d49d7eff98e2d784774d9bb403b6e1a541c873e3f2151e2aa10
|
Provenance
The following attestation bundles were made for lojack_api-0.7.1-py3-none-any.whl:
Publisher:
publish.yml on devinslick/lojack_api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lojack_api-0.7.1-py3-none-any.whl -
Subject digest:
a492f534b8f86c6b8a66c572cd4083233ab34342093d9962e7a9dee1bdc330cd - Sigstore transparency entry: 927296378
- Sigstore integration time:
-
Permalink:
devinslick/lojack_api@2a15e420a3bb8ca3dc4f5a80d1b23d4b35f5d591 -
Branch / Tag:
refs/tags/0.7.1 - Owner: https://github.com/devinslick
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2a15e420a3bb8ca3dc4f5a80d1b23d4b35f5d591 -
Trigger Event:
release
-
Statement type: