Add sensor entities to Traccar Server (#111374)
This commit is contained in:
parent
e473914407
commit
a1ae4ec23d
12 changed files with 318 additions and 41 deletions
|
@ -1482,6 +1482,7 @@ omit =
|
|||
homeassistant/components/traccar_server/device_tracker.py
|
||||
homeassistant/components/traccar_server/entity.py
|
||||
homeassistant/components/traccar_server/helpers.py
|
||||
homeassistant/components/traccar_server/sensor.py
|
||||
homeassistant/components/tractive/__init__.py
|
||||
homeassistant/components/tractive/binary_sensor.py
|
||||
homeassistant/components/tractive/device_tracker.py
|
||||
|
|
|
@ -30,7 +30,7 @@ from .const import (
|
|||
)
|
||||
from .coordinator import TraccarServerCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER]
|
||||
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
|
|
@ -10,12 +10,8 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
ATTR_ADDRESS,
|
||||
ATTR_ALTITUDE,
|
||||
ATTR_CATEGORY,
|
||||
ATTR_GEOFENCE,
|
||||
ATTR_MOTION,
|
||||
ATTR_SPEED,
|
||||
ATTR_STATUS,
|
||||
ATTR_TRACCAR_ID,
|
||||
ATTR_TRACKER,
|
||||
|
@ -44,23 +40,13 @@ class TraccarServerDeviceTracker(TraccarServerEntity, TrackerEntity):
|
|||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int:
|
||||
"""Return battery value of the device."""
|
||||
return self.traccar_position["attributes"].get("batteryLevel", -1)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device specific attributes."""
|
||||
geofence_name = self.traccar_geofence["name"] if self.traccar_geofence else None
|
||||
return {
|
||||
**self.traccar_attributes,
|
||||
ATTR_ADDRESS: self.traccar_position["address"],
|
||||
ATTR_ALTITUDE: self.traccar_position["altitude"],
|
||||
ATTR_CATEGORY: self.traccar_device["category"],
|
||||
ATTR_GEOFENCE: geofence_name,
|
||||
ATTR_MOTION: self.traccar_position["attributes"].get("motion", False),
|
||||
ATTR_SPEED: self.traccar_position["speed"],
|
||||
ATTR_STATUS: self.traccar_device["status"],
|
||||
ATTR_TRACCAR_ID: self.traccar_device["id"],
|
||||
ATTR_TRACKER: DOMAIN,
|
||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.components.diagnostics import REDACTED, async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -13,21 +13,23 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|||
from .const import DOMAIN
|
||||
from .coordinator import TraccarServerCoordinator
|
||||
|
||||
TO_REDACT = {
|
||||
KEYS_TO_REDACT = {
|
||||
"area", # This is the polygon area of a geofence
|
||||
CONF_ADDRESS,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
"area", # This is the polygon area of a geofence
|
||||
}
|
||||
|
||||
|
||||
def _entity_state(
|
||||
hass: HomeAssistant,
|
||||
entity: er.RegistryEntry,
|
||||
coordinator: TraccarServerCoordinator,
|
||||
) -> dict[str, Any] | None:
|
||||
states_to_redact = {x["position"]["address"] for x in coordinator.data.values()}
|
||||
return (
|
||||
{
|
||||
"state": state.state,
|
||||
"state": state.state if state.state not in states_to_redact else REDACTED,
|
||||
"attributes": state.attributes,
|
||||
}
|
||||
if (state := hass.states.get(entity.entity_id))
|
||||
|
@ -57,12 +59,13 @@ async def async_get_config_entry_diagnostics(
|
|||
{
|
||||
"enity_id": entity.entity_id,
|
||||
"disabled": entity.disabled,
|
||||
"state": _entity_state(hass, entity),
|
||||
"unit_of_measurement": entity.unit_of_measurement,
|
||||
"state": _entity_state(hass, entity, coordinator),
|
||||
}
|
||||
for entity in entities
|
||||
],
|
||||
},
|
||||
TO_REDACT,
|
||||
KEYS_TO_REDACT,
|
||||
)
|
||||
|
||||
|
||||
|
@ -81,6 +84,7 @@ async def async_get_device_diagnostics(
|
|||
include_disabled_entities=True,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
return async_redact_data(
|
||||
{
|
||||
"subscription_status": coordinator.client.subscription_status,
|
||||
|
@ -90,10 +94,11 @@ async def async_get_device_diagnostics(
|
|||
{
|
||||
"enity_id": entity.entity_id,
|
||||
"disabled": entity.disabled,
|
||||
"state": _entity_state(hass, entity),
|
||||
"unit_of_measurement": entity.unit_of_measurement,
|
||||
"state": _entity_state(hass, entity, coordinator),
|
||||
}
|
||||
for entity in entities
|
||||
],
|
||||
},
|
||||
TO_REDACT,
|
||||
KEYS_TO_REDACT,
|
||||
)
|
||||
|
|
15
homeassistant/components/traccar_server/icons.json
Normal file
15
homeassistant/components/traccar_server/icons.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"altitude": {
|
||||
"default": "mdi:altimeter"
|
||||
},
|
||||
"address": {
|
||||
"default": "mdi:map-marker"
|
||||
},
|
||||
"geofence": {
|
||||
"default": "mdi:map-marker"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
125
homeassistant/components/traccar_server/sensor.py
Normal file
125
homeassistant/components/traccar_server/sensor.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
"""Support for Traccar server sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, Literal, TypeVar, cast
|
||||
|
||||
from pytraccar import DeviceModel, GeofenceModel, PositionModel
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfSpeed
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TraccarServerCoordinator
|
||||
from .entity import TraccarServerEntity
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TraccarServerSensorEntityDescription(Generic[_T], SensorEntityDescription):
|
||||
"""Describe Traccar Server sensor entity."""
|
||||
|
||||
data_key: Literal["position", "device", "geofence", "attributes"]
|
||||
entity_registry_enabled_default = False
|
||||
entity_category = EntityCategory.DIAGNOSTIC
|
||||
value_fn: Callable[[_T], StateType]
|
||||
|
||||
|
||||
TRACCAR_SERVER_SENSOR_ENTITY_DESCRIPTIONS = (
|
||||
TraccarServerSensorEntityDescription[PositionModel](
|
||||
key="attributes.batteryLevel",
|
||||
data_key="position",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda x: x["attributes"].get("batteryLevel", -1),
|
||||
),
|
||||
TraccarServerSensorEntityDescription[PositionModel](
|
||||
key="speed",
|
||||
data_key="position",
|
||||
device_class=SensorDeviceClass.SPEED,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.KNOTS,
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda x: x["speed"],
|
||||
),
|
||||
TraccarServerSensorEntityDescription[PositionModel](
|
||||
key="altitude",
|
||||
data_key="position",
|
||||
translation_key="altitude",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
suggested_display_precision=1,
|
||||
value_fn=lambda x: x["altitude"],
|
||||
),
|
||||
TraccarServerSensorEntityDescription[PositionModel](
|
||||
key="address",
|
||||
data_key="position",
|
||||
translation_key="address",
|
||||
value_fn=lambda x: x["address"],
|
||||
),
|
||||
TraccarServerSensorEntityDescription[GeofenceModel | None](
|
||||
key="name",
|
||||
data_key="geofence",
|
||||
translation_key="geofence",
|
||||
value_fn=lambda x: x["name"] if x else None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensor entities."""
|
||||
coordinator: TraccarServerCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
TraccarServerSensor(
|
||||
coordinator=coordinator,
|
||||
device=entry["device"],
|
||||
description=cast(TraccarServerSensorEntityDescription, description),
|
||||
)
|
||||
for entry in coordinator.data.values()
|
||||
for description in TRACCAR_SERVER_SENSOR_ENTITY_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class TraccarServerSensor(TraccarServerEntity, SensorEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: TraccarServerSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TraccarServerCoordinator,
|
||||
device: DeviceModel,
|
||||
description: TraccarServerSensorEntityDescription[_T],
|
||||
) -> None:
|
||||
"""Initialize the Traccar Server sensor."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{device['uniqueId']}_{description.data_key}_{description.key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
getattr(self, f"traccar_{self.entity_description.data_key}")
|
||||
)
|
|
@ -41,5 +41,18 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"address": {
|
||||
"name": "Address"
|
||||
},
|
||||
"altitude": {
|
||||
"name": "Altitude"
|
||||
},
|
||||
"geofence": {
|
||||
"name": "Geofence"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"id": 0,
|
||||
"name": "X-Wing",
|
||||
"uniqueId": "abc123",
|
||||
"status": "unknown",
|
||||
"status": "online",
|
||||
"disabled": false,
|
||||
"lastUpdate": "1970-01-01T00:00:00Z",
|
||||
"positionId": 0,
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
"network": {},
|
||||
"geofenceIds": [0],
|
||||
"attributes": {
|
||||
"custom_attr_1": "custom_attr_1_value"
|
||||
"custom_attr_1": "custom_attr_1_value",
|
||||
"batteryLevel": 15.00000867601
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -17,5 +17,7 @@
|
|||
"coordinateFormat": null,
|
||||
"openIdEnabled": true,
|
||||
"openIdForce": true,
|
||||
"attributes": {}
|
||||
"attributes": {
|
||||
"speedUnit": "kn"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
'name': 'X-Wing',
|
||||
'phone': None,
|
||||
'positionId': 0,
|
||||
'status': 'unknown',
|
||||
'status': 'online',
|
||||
'uniqueId': 'abc123',
|
||||
}),
|
||||
'geofence': dict({
|
||||
|
@ -47,6 +47,7 @@
|
|||
'address': '**REDACTED**',
|
||||
'altitude': 546841384638,
|
||||
'attributes': dict({
|
||||
'batteryLevel': 15.00000867601,
|
||||
'custom_attr_1': 'custom_attr_1_value',
|
||||
}),
|
||||
'course': 360,
|
||||
|
@ -75,25 +76,84 @@
|
|||
'enity_id': 'device_tracker.x_wing',
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'address': '**REDACTED**',
|
||||
'altitude': 546841384638,
|
||||
'battery_level': -1,
|
||||
'category': 'starfighter',
|
||||
'custom_attr_1': 'custom_attr_1_value',
|
||||
'friendly_name': 'X-Wing',
|
||||
'geofence': 'Tatooine',
|
||||
'gps_accuracy': 3.5,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'motion': False,
|
||||
'source_type': 'gps',
|
||||
'speed': 4568795,
|
||||
'status': 'unknown',
|
||||
'status': 'online',
|
||||
'traccar_id': 0,
|
||||
'tracker': 'traccar_server',
|
||||
}),
|
||||
'state': 'not_home',
|
||||
}),
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'enity_id': 'sensor.x_wing_battery',
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'X-Wing Battery',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'state': '15.00000867601',
|
||||
}),
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'enity_id': 'sensor.x_wing_speed',
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'device_class': 'speed',
|
||||
'friendly_name': 'X-Wing Speed',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': 'kn',
|
||||
}),
|
||||
'state': '4568795',
|
||||
}),
|
||||
'unit_of_measurement': 'kn',
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'enity_id': 'sensor.x_wing_altitude',
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'friendly_name': 'X-Wing Altitude',
|
||||
'state_class': 'measurement',
|
||||
'unit_of_measurement': 'm',
|
||||
}),
|
||||
'state': '546841384638',
|
||||
}),
|
||||
'unit_of_measurement': 'm',
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'enity_id': 'sensor.x_wing_address',
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'friendly_name': 'X-Wing Address',
|
||||
}),
|
||||
'state': '**REDACTED**',
|
||||
}),
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'enity_id': 'sensor.x_wing_geofence',
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'friendly_name': 'X-Wing Geofence',
|
||||
}),
|
||||
'state': 'Tatooine',
|
||||
}),
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
]),
|
||||
'subscription_status': 'disconnected',
|
||||
|
@ -130,7 +190,7 @@
|
|||
'name': 'X-Wing',
|
||||
'phone': None,
|
||||
'positionId': 0,
|
||||
'status': 'unknown',
|
||||
'status': 'online',
|
||||
'uniqueId': 'abc123',
|
||||
}),
|
||||
'geofence': dict({
|
||||
|
@ -147,6 +207,7 @@
|
|||
'address': '**REDACTED**',
|
||||
'altitude': 546841384638,
|
||||
'attributes': dict({
|
||||
'batteryLevel': 15.00000867601,
|
||||
'custom_attr_1': 'custom_attr_1_value',
|
||||
}),
|
||||
'course': 360,
|
||||
|
@ -170,10 +231,41 @@
|
|||
}),
|
||||
}),
|
||||
'entities': list([
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_battery',
|
||||
'state': None,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_speed',
|
||||
'state': None,
|
||||
'unit_of_measurement': 'kn',
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_altitude',
|
||||
'state': None,
|
||||
'unit_of_measurement': 'm',
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_address',
|
||||
'state': None,
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_geofence',
|
||||
'state': None,
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'device_tracker.x_wing',
|
||||
'state': None,
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
]),
|
||||
'subscription_status': 'disconnected',
|
||||
|
@ -210,7 +302,7 @@
|
|||
'name': 'X-Wing',
|
||||
'phone': None,
|
||||
'positionId': 0,
|
||||
'status': 'unknown',
|
||||
'status': 'online',
|
||||
'uniqueId': 'abc123',
|
||||
}),
|
||||
'geofence': dict({
|
||||
|
@ -227,6 +319,7 @@
|
|||
'address': '**REDACTED**',
|
||||
'altitude': 546841384638,
|
||||
'attributes': dict({
|
||||
'batteryLevel': 15.00000867601,
|
||||
'custom_attr_1': 'custom_attr_1_value',
|
||||
}),
|
||||
'course': 360,
|
||||
|
@ -250,30 +343,56 @@
|
|||
}),
|
||||
}),
|
||||
'entities': list([
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_battery',
|
||||
'state': None,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_speed',
|
||||
'state': None,
|
||||
'unit_of_measurement': 'kn',
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_altitude',
|
||||
'state': None,
|
||||
'unit_of_measurement': 'm',
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_address',
|
||||
'state': None,
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'disabled': True,
|
||||
'enity_id': 'sensor.x_wing_geofence',
|
||||
'state': None,
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
dict({
|
||||
'disabled': False,
|
||||
'enity_id': 'device_tracker.x_wing',
|
||||
'state': dict({
|
||||
'attributes': dict({
|
||||
'address': '**REDACTED**',
|
||||
'altitude': 546841384638,
|
||||
'battery_level': -1,
|
||||
'category': 'starfighter',
|
||||
'custom_attr_1': 'custom_attr_1_value',
|
||||
'friendly_name': 'X-Wing',
|
||||
'geofence': 'Tatooine',
|
||||
'gps_accuracy': 3.5,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'motion': False,
|
||||
'source_type': 'gps',
|
||||
'speed': 4568795,
|
||||
'status': 'unknown',
|
||||
'status': 'online',
|
||||
'traccar_id': 0,
|
||||
'tracker': 'traccar_server',
|
||||
}),
|
||||
'state': 'not_home',
|
||||
}),
|
||||
'unit_of_measurement': None,
|
||||
}),
|
||||
]),
|
||||
'subscription_status': 'disconnected',
|
||||
|
|
|
@ -44,6 +44,7 @@ async def test_device_diagnostics(
|
|||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test device diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
@ -58,6 +59,15 @@ async def test_device_diagnostics(
|
|||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, mock_config_entry.entry_id
|
||||
):
|
||||
entities = er.async_entries_for_device(
|
||||
entity_registry,
|
||||
device_id=device.id,
|
||||
include_disabled_entities=True,
|
||||
)
|
||||
# Enable all entitits to show everything in snapshots
|
||||
for entity in entities:
|
||||
entity_registry.async_update_entity(entity.entity_id, disabled_by=None)
|
||||
|
||||
result = await get_diagnostics_for_device(
|
||||
hass, hass_client, mock_config_entry, device=device
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue