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

Add Traccar server integration #109002

Merged
merged 4 commits into from Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .coveragerc
Expand Up @@ -1414,6 +1414,11 @@ omit =
homeassistant/components/tplink_omada/controller.py
homeassistant/components/tplink_omada/update.py
homeassistant/components/traccar/device_tracker.py
homeassistant/components/traccar_server/__init__.py
homeassistant/components/traccar_server/coordinator.py
homeassistant/components/traccar_server/device_tracker.py
homeassistant/components/traccar_server/entity.py
homeassistant/components/traccar_server/helpers.py
homeassistant/components/tractive/__init__.py
homeassistant/components/tractive/binary_sensor.py
homeassistant/components/tractive/device_tracker.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -1388,6 +1388,8 @@ build.json @home-assistant/supervisor
/tests/components/tplink_omada/ @MarkGodwin
/homeassistant/components/traccar/ @ludeeus
/tests/components/traccar/ @ludeeus
/homeassistant/components/traccar_server/ @ludeeus
/tests/components/traccar_server/ @ludeeus
/homeassistant/components/trace/ @home-assistant/core
/tests/components/trace/ @home-assistant/core
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
Expand Down
70 changes: 70 additions & 0 deletions homeassistant/components/traccar_server/__init__.py
@@ -0,0 +1,70 @@
"""The Traccar Server integration."""
from __future__ import annotations

from pytraccar import ApiClient

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import (
CONF_CUSTOM_ATTRIBUTES,
CONF_EVENTS,
CONF_MAX_ACCURACY,
CONF_SKIP_ACCURACY_FILTER_FOR,
DOMAIN,
)
from .coordinator import TraccarServerCoordinator

PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Traccar Server from a config entry."""
coordinator = TraccarServerCoordinator(
hass=hass,
client=ApiClient(
client_session=async_get_clientsession(hass),
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
ssl=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
),
events=entry.options.get(CONF_EVENTS, []),
max_accuracy=entry.options.get(CONF_MAX_ACCURACY, 0.0),
skip_accuracy_filter_for=entry.options.get(CONF_SKIP_ACCURACY_FILTER_FOR, []),
custom_attributes=entry.options.get(CONF_CUSTOM_ATTRIBUTES, []),
)

await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
Comment on lines +52 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle an options update."""
await hass.config_entries.async_reload(entry.entry_id)
168 changes: 168 additions & 0 deletions homeassistant/components/traccar_server/config_flow.py
@@ -0,0 +1,168 @@
"""Config flow for Traccar Server integration."""
from __future__ import annotations

from typing import Any

from pytraccar import ApiClient, ServerModel, TraccarException
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from homeassistant.helpers.selector import (
BooleanSelector,
BooleanSelectorConfig,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)

from .const import (
CONF_CUSTOM_ATTRIBUTES,
CONF_EVENTS,
CONF_MAX_ACCURACY,
CONF_SKIP_ACCURACY_FILTER_FOR,
DOMAIN,
EVENTS,
LOGGER,
)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): TextSelector(
TextSelectorConfig(type=TextSelectorType.TEXT)
),
vol.Optional(CONF_PORT, default="8082"): TextSelector(
TextSelectorConfig(type=TextSelectorType.TEXT)
),
vol.Required(CONF_USERNAME): TextSelector(
TextSelectorConfig(type=TextSelectorType.EMAIL)
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
vol.Optional(CONF_SSL, default=False): BooleanSelector(BooleanSelectorConfig()),
vol.Optional(CONF_VERIFY_SSL, default=True): BooleanSelector(
BooleanSelectorConfig()
),
}
)

OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
schema=vol.Schema(
{
vol.Optional(CONF_MAX_ACCURACY, default=0.0): NumberSelector(
NumberSelectorConfig(
mode=NumberSelectorMode.BOX,
min=0.0,
)
),
vol.Optional(CONF_CUSTOM_ATTRIBUTES, default=[]): SelectSelector(
SelectSelectorConfig(
mode=SelectSelectorMode.DROPDOWN,
multiple=True,
sort=True,
custom_value=True,
options=[],
)
),
vol.Optional(CONF_SKIP_ACCURACY_FILTER_FOR, default=[]): SelectSelector(
SelectSelectorConfig(
mode=SelectSelectorMode.DROPDOWN,
multiple=True,
sort=True,
custom_value=True,
options=[],
)
),
vol.Optional(CONF_EVENTS, default=[]): SelectSelector(
SelectSelectorConfig(
mode=SelectSelectorMode.DROPDOWN,
multiple=True,
sort=True,
custom_value=True,
options=list(EVENTS),
)
),
}
)
),
}


class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Traccar Server."""

async def _get_server_info(self, user_input: dict[str, Any]) -> ServerModel:
"""Get server info."""
client = ApiClient(
client_session=async_get_clientsession(self.hass),
host=user_input[CONF_HOST],
port=user_input[CONF_PORT],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
ssl=user_input[CONF_SSL],
verify_ssl=user_input[CONF_VERIFY_SSL],
)
return await client.get_server()

async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match(
{
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
}
)
try:
await self._get_server_info(user_input)
except TraccarException as exception:
LOGGER.error("Unable to connect to Traccar Server: %s", exception)
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}",
data=user_input,
)

return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> SchemaOptionsFlowHandler:
"""Get the options flow for this handler."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
39 changes: 39 additions & 0 deletions homeassistant/components/traccar_server/const.py
@@ -0,0 +1,39 @@
"""Constants for the Traccar Server integration."""
from logging import getLogger

DOMAIN = "traccar_server"
LOGGER = getLogger(__package__)

ATTR_ADDRESS = "address"
ATTR_ALTITUDE = "altitude"
ATTR_CATEGORY = "category"
ATTR_GEOFENCE = "geofence"
ATTR_MOTION = "motion"
ATTR_SPEED = "speed"
ATTR_STATUS = "status"
ATTR_TRACKER = "tracker"
ATTR_TRACCAR_ID = "traccar_id"

CONF_MAX_ACCURACY = "max_accuracy"
CONF_CUSTOM_ATTRIBUTES = "custom_attributes"
CONF_EVENTS = "events"
CONF_SKIP_ACCURACY_FILTER_FOR = "skip_accuracy_filter_for"

EVENTS = {
"deviceMoving": "device_moving",
"commandResult": "command_result",
"deviceFuelDrop": "device_fuel_drop",
"geofenceEnter": "geofence_enter",
"deviceOffline": "device_offline",
"driverChanged": "driver_changed",
"geofenceExit": "geofence_exit",
"deviceOverspeed": "device_overspeed",
"deviceOnline": "device_online",
"deviceStopped": "device_stopped",
"maintenance": "maintenance",
"alarm": "alarm",
"textMessage": "text_message",
"deviceUnknown": "device_unknown",
"ignitionOff": "ignition_off",
"ignitionOn": "ignition_on",
}