Skip to main content

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.

Tests codecov PyPI - Downloads

Features

  • Async-first design - Built with asyncio and aiohttp for non-blocking I/O
  • No httpx dependency - Uses aiohttp to 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.typed marker
  • 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 sent
  • None - 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 token
  • X-Nspire-Correlationid - Unique request ID
  • X-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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lojack_api-0.7.2.tar.gz (49.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lojack_api-0.7.2-py3-none-any.whl (30.2 kB view details)

Uploaded Python 3

File details

Details for the file lojack_api-0.7.2.tar.gz.

File metadata

  • Download URL: lojack_api-0.7.2.tar.gz
  • Upload date:
  • Size: 49.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for lojack_api-0.7.2.tar.gz
Algorithm Hash digest
SHA256 0fe26fde844f96b26b0f5ae42f100cc3e4ea5aafefd4dcb76bfd7faccc1a0655
MD5 b22ea5929bc6aaddbb07759d85a813e0
BLAKE2b-256 756de502f1c724a595c1161c6de4fc9a92e5b9d1dae2f7dcd7861c8cd8adb43b

See more details on using hashes here.

Provenance

The following attestation bundles were made for lojack_api-0.7.2.tar.gz:

Publisher: publish.yml on devinslick/lojack_api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lojack_api-0.7.2-py3-none-any.whl.

File metadata

  • Download URL: lojack_api-0.7.2-py3-none-any.whl
  • Upload date:
  • Size: 30.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for lojack_api-0.7.2-py3-none-any.whl
Algorithm Hash digest
SHA256 391cdbe41f0a9d1758c4101a4e49590c70af94d5f3b46c2dcb6557db1c7f028e
MD5 b356e9ecce03d85a3106f58ca7ee4dac
BLAKE2b-256 138792cb81a1c40bdb6067331961488079bb3068ebc2c751f863b8d7fb19f275

See more details on using hashes here.

Provenance

The following attestation bundles were made for lojack_api-0.7.2-py3-none-any.whl:

Publisher: publish.yml on devinslick/lojack_api

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page