"""Data-types returned by Invoxia API."""
# pylint: disable=too-few-public-methods
# This file only defines attrs dataclasses which do not
# have to satisfy the minimum public-method count.
from __future__ import annotations
import enum
import uuid
from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional, Type
from .exceptions import UnknownDeviceType
try:
import attrs
except ModuleNotFoundError:
# Handle attrs<21.3.0
import attr as attrs # type: ignore[no-redef]
def _date_converter(val: Optional[str]) -> Optional[datetime]:
"""Converts a datetime in RFC3339 format to datetime object."""
if val is None:
return None
try:
return datetime.strptime(val, "%Y-%m-%dT%H:%M:%S.%f%z")
except ValueError:
# Error will be raised if value does not match format
# This mainly happens if date does not contain UTC offset
return datetime.strptime(f"{val}Z", "%Y-%m-%dT%H:%M:%S.%f%z")
def _date_repr(val: Optional[datetime]) -> str:
"""Print a datetime in a compact format."""
if val is None:
return str(val)
return val.isoformat()
[docs]@attrs.define(auto_attribs=True)
class User:
"""User definition as returned by Invoxia API."""
id: int = attrs.field(converter=int)
"""User unique identifier."""
username: str = attrs.field(converter=str)
"""User name."""
profiles: List[int] = attrs.field(converter=lambda l: [int(i) for i in l])
"""List of profile ids associated to user."""
[docs]@attrs.define(auto_attribs=False)
class Device:
"""Base class for devices."""
_registry: Dict[str, Type[Device]] = {}
id: int = attrs.field(converter=int)
"""Device unique identifier."""
created: datetime = attrs.field(converter=_date_converter, repr=_date_repr)
"""Datetime of device registration."""
name: str = attrs.field(converter=str)
"""Device name."""
timezone: str = attrs.field(converter=str)
"""Timezone associated to device."""
version: str = attrs.field(converter=str)
"""Device version."""
def __init_subclass__(cls, dtype: Optional[str] = None):
"""Register subclasses with their given type."""
if dtype is not None:
Device._registry[dtype] = cls
[docs] @staticmethod
def get(device_data: dict[str, Any]) -> Device:
"""Generate a Device object based on its type."""
if device_data["type"] not in Device._registry:
raise UnknownDeviceType(device_data)
return Device._registry[device_data.pop("type")](**device_data)
[docs] @classmethod
def get_types(cls) -> Iterable[str]:
"""Return list of registered device types."""
return cls._registry.keys()
[docs]@attrs.define(slots=False)
class Android(Device, dtype="android"):
"""Definition of devices of type 'android'."""
serial: uuid.UUID = attrs.field(converter=uuid.UUID)
"""Device Universally Unique Identifier"""
[docs]@attrs.define(slots=False)
class Iphone(Device, dtype="iphone"):
"""Definition of devices of type 'iphone'."""
serial: uuid.UUID = attrs.field(converter=uuid.UUID)
"""Device Universally Unique Identifier"""
[docs]class TrackerIcon(enum.Enum):
"""Enumeration of tracker icons."""
OTHER = 0 # 33
HANDBAG = 1 # 1
BRIEFCASE = 2 # 2
SUITCASE = 3 # 3
BACKPACK = 5 # 5
BIKE = 7 # 7
BOAT = 8 # 8
CAR = 10 # 10
CARAVAN = 12
CART = 13
KAYAK = 15
LAPTOP = 16
MOTO = 17
HELICOPTER = 18
PLANE = 19
SCOOTER = 21
TENT = 23
TRUCK = 24
TRACTOR = 26
DOG = 30
CAT = 31 # 31
PERSON = 32
GIRL = 34
BACKHOE_LOADER = 36
ANIMAL = 37
WOMAN = 38
MAN = 39
EBIKE = 40 # 40
BEEHIVE = 41 # 41
CARPARK = 42
ANTENNA = 43
HEALTH = 44
KEYS = 46
TV = 47
PHONE = 48 # 48
[docs]class TrackerMode(enum.Enum):
"""Enumeration of possible tracker modes."""
DAILY = 1
INTENSE = 2
CHILD = 3
KEEP_ALIVE = 4
AIRPLANE = 6
LOST = 7
VEHICLE_S1 = 8
VEHICLE_S2 = 9
VEHICLE_S3 = 10
DAILY_PET = 21
INTENSE_PET = 22
CHILD_PET = 23
LOST_PET_DEBUG = 24
LOST_PET = 27
LWT3 = 235
[docs]class TrackerUsage(enum.Enum):
"""Enumeration of possible tracker usages."""
BIKE = "bike"
CAT = "cat"
CHILD = "child"
DOG = "dog"
MOTO = "moto"
OTHER = "other"
PERSON = "person"
PET = "pet"
TOOL = "tool"
UND = "" # Undefined
VEHICLE = "vehicle"
[docs]@attrs.define(auto_attribs=True)
class TrackerConfig:
"""Definition of tracker config data."""
board_name: str = attrs.field(converter=str)
"""Tracker board-model reference."""
color: int = attrs.field(converter=int)
"""Icon color."""
firmware_path: str = attrs.field(converter=str)
"""URL to firmware update (To be confirmed)."""
icon: TrackerIcon = attrs.field(converter=TrackerIcon)
"""ID of the icon selected for this tracker."""
mode: TrackerMode = attrs.field(converter=lambda val: TrackerMode(int(val)))
"""Operating mode of the tracker."""
network: str = attrs.field(converter=str)
"""Network used by tracker (LoRa/SigFox)."""
network_config: int = attrs.field(converter=int)
"""To be determined."""
network_region: str = attrs.field(converter=str)
"""Region configured for the network (To be confirmed)."""
notify_position: bool = attrs.field(converter=bool)
"""Whether notifications must be send when tracker position changes."""
notify_long_walk: bool = attrs.field(converter=bool)
"""To be determined. Probably related to pet mode."""
usage: TrackerUsage = attrs.field(converter=TrackerUsage)
"""Tracker usage defined during device setup."""
image: Optional[str] = attrs.field(
validator=attrs.validators.optional(attrs.validators.instance_of(str)),
default=None,
)
"""Image reference when icon as been replaced by a user-defined image."""
weight: Optional[float] = attrs.field(
converter=attrs.converters.optional(float),
validator=attrs.validators.optional(attrs.validators.instance_of(float)),
default=None,
)
"""To be determined. Probably weight of object to which tracker is attached."""
[docs]@attrs.define(auto_attribs=True)
class TrackerStatus:
"""Definition of tracker status data."""
battery: int = attrs.field(converter=int)
begin_date: datetime = attrs.field(converter=_date_converter, repr=_date_repr)
network_operator: str = attrs.field(converter=str)
state: str = attrs.field(converter=str)
stationary: int = attrs.field(converter=int)
sub_end_date: str = attrs.field(converter=str)
sub_state: str = attrs.field(converter=str)
last_event: Optional[datetime] = attrs.field(
converter=_date_converter,
validator=attrs.validators.optional(attrs.validators.instance_of(datetime)),
default=None,
repr=_date_repr,
)
last_location: Optional[datetime] = attrs.field(
converter=_date_converter,
validator=attrs.validators.optional(attrs.validators.instance_of(datetime)),
default=None,
repr=_date_repr,
)
[docs]class TrackerMethod(enum.Enum):
"""Enumeration of tracking methods."""
UNKNOWN = 0
NETWORK = 1
GPS = 2
BSSID = 3
PLACE = 4
UBISCALE = 5
PHONE = 6
BSSID_TGU = 7
SERVER = 8
BSSID2 = 9
BSSID3 = 10
BSSID4 = 11
FLEET_CORRECTION = 12
BLE_NEIGHBOR = 13
GPS_BLE = 14
PHONE2 = 15
[docs]@attrs.define(auto_attribs=True)
class TrackerData:
"""Definition of tracker location data."""
datetime: datetime = attrs.field(converter=_date_converter, repr=_date_repr)
"""Datetime of location measurement."""
lat: float = attrs.field(converter=float)
"""Device latitude."""
lng: float = attrs.field(converter=float)
"""Device longitude."""
method: TrackerMethod = attrs.field(converter=TrackerMethod)
"""Method used for location acquisition."""
pkt_drop: int = attrs.field(converter=int)
"""To be determined. (Probably number of packet drop since last location)."""
precision: int = attrs.field(converter=int)
"""Precision of location measurement (To be confirmed)."""
uuid: uuid.UUID = attrs.field(converter=uuid.UUID)
"""Universally unique identifier of location data."""
def _tracker_config_converter(val: Dict[str, Any]) -> TrackerConfig:
"""Converter to form a TrackerConfig from its JSON representation."""
return TrackerConfig(**val)
def _tracker_status_converter(val: Dict[str, Any]) -> TrackerStatus:
"""Converter to form a TrackerStatus from its JSON representation."""
return TrackerStatus(**val)
[docs]class Tracker(Device, dtype="tracker"):
"""Base class for trackers."""
[docs]@attrs.define(slots=False)
class Tracker01(Tracker, dtype="tracker_01"):
"""Definition of devices of type 'tracker_01'."""
serial: str = attrs.field(converter=str)
"""Tracker serial number."""
version_build: str = attrs.field(converter=str)
"""Firmware build reference (To be confirmed)."""
tracker_config: TrackerConfig = attrs.field(converter=_tracker_config_converter)
"""Tracker configuration data."""
tracker_status: TrackerStatus = attrs.field(converter=_tracker_status_converter)
"""Tracker current status."""
model: Optional[str] = attrs.field(
validator=attrs.validators.optional(attrs.validators.instance_of(str)),
default=None,
)
"""Tracker model."""