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

Import Traccar YAML configuration to Traccar Server #109226

Merged
merged 2 commits into from Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
259 changes: 68 additions & 191 deletions homeassistant/components/traccar/device_tracker.py
@@ -1,30 +1,25 @@
"""Support for Traccar device tracking."""
from __future__ import annotations

import asyncio
from datetime import timedelta
import logging
from typing import Any

from pytraccar import (
ApiClient,
DeviceModel,
GeofenceModel,
PositionModel,
TraccarAuthenticationException,
TraccarConnectionException,
TraccarException,
)
from stringcase import camelcase
from pytraccar import ApiClient, TraccarException
import voluptuous as vol

from homeassistant.components.device_tracker import (
CONF_SCAN_INTERVAL,
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
AsyncSeeCallback,
SourceType,
TrackerEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.components.device_tracker.legacy import (
YAML_DEVICES,
remove_device_from_config,
)
from homeassistant.config import load_yaml_config_file
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_EVENT,
CONF_HOST,
Expand All @@ -34,34 +29,34 @@
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
Event,
HomeAssistant,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util, slugify
from homeassistant.util import slugify

from . import DOMAIN, TRACKER_UPDATE
from .const import (
ATTR_ACCURACY,
ATTR_ADDRESS,
ATTR_ALTITUDE,
ATTR_BATTERY,
ATTR_BEARING,
ATTR_CATEGORY,
ATTR_GEOFENCE,
ATTR_LATITUDE,
ATTR_LONGITUDE,
ATTR_MOTION,
ATTR_SPEED,
ATTR_STATUS,
ATTR_TRACCAR_ID,
ATTR_TRACKER,
CONF_MAX_ACCURACY,
CONF_SKIP_ACCURACY_ON,
EVENT_ALARM,
Expand Down Expand Up @@ -178,7 +173,7 @@ async def async_setup_scanner(
async_see: AsyncSeeCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> bool:
"""Validate the configuration and return a Traccar scanner."""
"""Import configuration to the new integration."""
api = ApiClient(
host=config[CONF_HOST],
port=config[CONF_PORT],
Expand All @@ -188,180 +183,62 @@ async def async_setup_scanner(
client_session=async_get_clientsession(hass, config[CONF_VERIFY_SSL]),
)

scanner = TraccarScanner(
api,
hass,
async_see,
config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
config[CONF_MAX_ACCURACY],
config[CONF_SKIP_ACCURACY_ON],
config[CONF_MONITORED_CONDITIONS],
config[CONF_EVENT],
)

return await scanner.async_init()


class TraccarScanner:
"""Define an object to retrieve Traccar data."""

def __init__(
self,
api: ApiClient,
hass: HomeAssistant,
async_see: AsyncSeeCallback,
scan_interval: timedelta,
max_accuracy: int,
skip_accuracy_on: bool,
custom_attributes: list[str],
event_types: list[str],
) -> None:
"""Initialize."""

if EVENT_ALL_EVENTS in event_types:
event_types = EVENTS
self._event_types = {camelcase(evt): evt for evt in event_types}
self._custom_attributes = custom_attributes
self._scan_interval = scan_interval
self._async_see = async_see
self._api = api
self._hass = hass
self._max_accuracy = max_accuracy
self._skip_accuracy_on = skip_accuracy_on
self._devices: list[DeviceModel] = []
self._positions: list[PositionModel] = []
self._geofences: list[GeofenceModel] = []

async def async_init(self):
"""Further initialize connection to Traccar."""
try:
await self._api.get_server()
except TraccarAuthenticationException:
_LOGGER.error("Authentication for Traccar failed")
return False
except TraccarConnectionException as exception:
_LOGGER.error("Connection with Traccar failed - %s", exception)
return False

await self._async_update()
async_track_time_interval(
self._hass, self._async_update, self._scan_interval, cancel_on_shutdown=True
)
return True

async def _async_update(self, now=None):
"""Update info from Traccar."""
_LOGGER.debug("Updating device data")
async def _run_import(_: Event):
known_devices: dict[str, dict[str, Any]] = {}
try:
(
self._devices,
self._positions,
self._geofences,
) = await asyncio.gather(
self._api.get_devices(),
self._api.get_positions(),
self._api.get_geofences(),
known_devices = await hass.async_add_executor_job(
load_yaml_config_file, hass.config.path(YAML_DEVICES)
)
except TraccarException as ex:
_LOGGER.error("Error while updating device data: %s", ex)
return

self._hass.async_create_task(self.import_device_data())
if self._event_types:
self._hass.async_create_task(self.import_events())

async def import_device_data(self):
"""Import device data from Traccar."""
for position in self._positions:
device = next(
(dev for dev in self._devices if dev["id"] == position["deviceId"]),
None,
except (FileNotFoundError, HomeAssistantError):
_LOGGER.debug(
"No valid known_devices.yaml found, "
"skip removal of devices from known_devices.yaml"
)

if not device:
continue

attr = {
ATTR_TRACKER: "traccar",
ATTR_ADDRESS: position["address"],
ATTR_SPEED: position["speed"],
ATTR_ALTITUDE: position["altitude"],
ATTR_MOTION: position["attributes"].get("motion", False),
ATTR_TRACCAR_ID: device["id"],
ATTR_GEOFENCE: next(
(
geofence["name"]
for geofence in self._geofences
if geofence["id"] in (position["geofenceIds"] or [])
),
None,
),
ATTR_CATEGORY: device["category"],
ATTR_STATUS: device["status"],
}

skip_accuracy_filter = False

for custom_attr in self._custom_attributes:
if device["attributes"].get(custom_attr) is not None:
attr[custom_attr] = position["attributes"][custom_attr]
if custom_attr in self._skip_accuracy_on:
skip_accuracy_filter = True
if position["attributes"].get(custom_attr) is not None:
attr[custom_attr] = position["attributes"][custom_attr]
if custom_attr in self._skip_accuracy_on:
skip_accuracy_filter = True

accuracy = position["accuracy"] or 0.0
if (
not skip_accuracy_filter
and self._max_accuracy > 0
and accuracy > self._max_accuracy
):
_LOGGER.debug(
"Excluded position by accuracy filter: %f (%s)",
accuracy,
attr[ATTR_TRACCAR_ID],
)
continue

await self._async_see(
dev_id=slugify(device["name"]),
gps=(position["latitude"], position["longitude"]),
gps_accuracy=accuracy,
battery=position["attributes"].get("batteryLevel", -1),
attributes=attr,
if known_devices:
traccar_devices: list[str] = []
try:
resp = await api.get_devices()
traccar_devices = [slugify(device["name"]) for device in resp]
except TraccarException as exception:
_LOGGER.error("Error while getting device data: %s", exception)
return

for dev_name in traccar_devices:
if dev_name in known_devices:
await hass.async_add_executor_job(
remove_device_from_config, hass, dev_name
)
_LOGGER.debug("Removed device %s from known_devices.yaml", dev_name)

if not hass.states.async_available(f"device_tracker.{dev_name}"):
hass.states.async_remove(f"device_tracker.{dev_name}")

hass.async_create_task(
hass.config_entries.flow.async_init(
"traccar_server",
context={"source": SOURCE_IMPORT},
data=config,
)
)

async def import_events(self):
"""Import events from Traccar."""
# get_reports_events requires naive UTC datetimes as of 1.0.0
start_intervel = dt_util.utcnow().replace(tzinfo=None)
events = await self._api.get_reports_events(
devices=[device["id"] for device in self._devices],
start_time=start_intervel,
end_time=start_intervel - self._scan_interval,
event_types=self._event_types.keys(),
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.8.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Traccar",
},
)
if events is not None:
for event in events:
self._hass.bus.async_fire(
f"traccar_{self._event_types.get(event['type'])}",
{
"device_traccar_id": event["deviceId"],
"device_name": next(
(
dev["name"]
for dev in self._devices
if dev["id"] == event["deviceId"]
),
None,
),
"type": event["type"],
"serverTime": event["eventTime"],
"attributes": event["attributes"],
},
)

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _run_import)
return True


class TraccarEntity(TrackerEntity, RestoreEntity):
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/traccar_server/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for Traccar Server integration."""
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from pytraccar import ApiClient, ServerModel, TraccarException
Expand Down Expand Up @@ -159,6 +160,39 @@ async def async_step_user(
errors=errors,
)

async def async_step_import(self, import_info: Mapping[str, Any]) -> FlowResult:
"""Import an entry."""
configured_port = str(import_info[CONF_PORT])
self._async_abort_entries_match(
{
CONF_HOST: import_info[CONF_HOST],
CONF_PORT: configured_port,
}
)
if "all_events" in (imported_events := import_info.get("event", [])):
events = list(EVENTS.values())
else:
events = imported_events
return self.async_create_entry(
title=f"{import_info[CONF_HOST]}:{configured_port}",
data={
CONF_HOST: import_info[CONF_HOST],
CONF_PORT: configured_port,
CONF_SSL: import_info.get(CONF_SSL, False),
CONF_VERIFY_SSL: import_info.get(CONF_VERIFY_SSL, True),
CONF_USERNAME: import_info[CONF_USERNAME],
CONF_PASSWORD: import_info[CONF_PASSWORD],
},
options={
CONF_MAX_ACCURACY: import_info[CONF_MAX_ACCURACY],
CONF_EVENTS: events,
CONF_CUSTOM_ATTRIBUTES: import_info.get("monitored_conditions", []),
CONF_SKIP_ACCURACY_FILTER_FOR: import_info.get(
"skip_accuracy_filter_on", []
),
},
)

@staticmethod
@callback
def async_get_options_flow(
Expand Down