mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 15:31:52 -05:00
Add ScorpionTrack heading sensor (#181601)
This commit is contained in:
@@ -4,6 +4,11 @@
|
||||
"vehicle_location": {
|
||||
"default": "mdi:car"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"heading": {
|
||||
"default": "mdi:compass"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ rules:
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: The created entities are useful for shared vehicle tracking and should be enabled by default.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfSpeed
|
||||
from homeassistant.const import DEGREE, EntityCategory, UnitOfSpeed
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -34,6 +34,15 @@ class ScorpionTrackSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
|
||||
SENSORS: tuple[ScorpionTrackSensorEntityDescription, ...] = (
|
||||
ScorpionTrackSensorEntityDescription(
|
||||
key="heading",
|
||||
translation_key="heading",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda vehicle: vehicle.position.bearing,
|
||||
),
|
||||
ScorpionTrackSensorEntityDescription(
|
||||
key="speed",
|
||||
device_class=SensorDeviceClass.SPEED,
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"heading": {
|
||||
"name": "Heading"
|
||||
},
|
||||
"last_reported": {
|
||||
"name": "Last reported"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,61 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor_snapshot[sensor.ab12_cde_heading-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.ab12_cde_heading',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Heading',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Heading',
|
||||
'platform': 'scorpiontrack',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'heading',
|
||||
'unique_id': '101_1_heading',
|
||||
'unit_of_measurement': '°',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.ab12_cde_heading-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'AB12 CDE Heading',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '°',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.ab12_cde_heading',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '182.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_snapshot[sensor.ab12_cde_last_reported-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import replace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyscorpiontrack import ScorpionTrackShare
|
||||
from pyscorpiontrack import ScorpionTrackConnectionError, ScorpionTrackShare
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -27,6 +27,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat
|
||||
|
||||
ENTITY_ID = "sensor.ab12_cde_speed"
|
||||
LAST_REPORTED_ENTITY_ID = "sensor.ab12_cde_last_reported"
|
||||
HEADING_ENTITY_ID = "sensor.ab12_cde_heading"
|
||||
|
||||
|
||||
async def test_speed_sensor_state(
|
||||
@@ -46,10 +47,12 @@ async def test_speed_sensor_state(
|
||||
mock_scorpiontrack_client.async_get_share.assert_awaited_once_with()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.freeze_time("2026-08-11 12:00:00+00:00")
|
||||
async def test_sensor_snapshot(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
@@ -58,6 +61,75 @@ async def test_sensor_snapshot(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
mock_scorpiontrack_client.async_get_share.assert_awaited_once_with()
|
||||
|
||||
|
||||
async def test_heading_sensor_disabled_by_default(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test heading is registered without creating a state until enabled."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entry = entity_registry.async_get(HEADING_ENTITY_ID)
|
||||
assert entry is not None
|
||||
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
assert hass.states.get(HEADING_ENTITY_ID) is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.parametrize(
|
||||
("bearing", "expected_state"),
|
||||
[
|
||||
pytest.param(0.0, "0.0", id="north"),
|
||||
pytest.param(None, STATE_UNKNOWN, id="missing"),
|
||||
],
|
||||
)
|
||||
async def test_heading_sensor_value(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
bearing: float | None,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test north is valid and a missing bearing is unknown."""
|
||||
vehicle = mock_share.vehicles[0]
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
mock_share,
|
||||
vehicles=(
|
||||
replace(vehicle, position=replace(vehicle.position, bearing=bearing)),
|
||||
),
|
||||
)
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(HEADING_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_heading_sensor_update_failure(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a failed share update makes heading unavailable."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
mock_scorpiontrack_client.async_get_share.side_effect = (
|
||||
ScorpionTrackConnectionError("Connection failed")
|
||||
)
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(HEADING_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_speed_sensor_metric_display(
|
||||
@@ -113,11 +185,13 @@ async def test_speed_sensor_availability(
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id",
|
||||
[
|
||||
pytest.param(ENTITY_ID, id="speed"),
|
||||
pytest.param(LAST_REPORTED_ENTITY_ID, id="last-reported"),
|
||||
pytest.param(HEADING_ENTITY_ID, id="heading"),
|
||||
],
|
||||
)
|
||||
async def test_removed_vehicle_makes_sensor_unavailable(
|
||||
@@ -143,23 +217,31 @@ async def test_removed_vehicle_makes_sensor_unavailable(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_speed_sensor_uses_existing_vehicle_device(
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id",
|
||||
[
|
||||
pytest.param(ENTITY_ID, id="speed"),
|
||||
pytest.param(HEADING_ENTITY_ID, id="heading"),
|
||||
],
|
||||
)
|
||||
async def test_sensor_uses_existing_vehicle_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test the speed sensor shares the vehicle device with the tracker."""
|
||||
"""Test the sensor shares the vehicle device with the tracker."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
speed_entry = entity_registry.async_get(ENTITY_ID)
|
||||
sensor_entry = entity_registry.async_get(entity_id)
|
||||
tracker_entry = entity_registry.async_get("device_tracker.ab12_cde")
|
||||
assert speed_entry is not None
|
||||
assert sensor_entry is not None
|
||||
assert tracker_entry is not None
|
||||
assert speed_entry.device_id == tracker_entry.device_id
|
||||
assert speed_entry.device_id is not None
|
||||
assert sensor_entry.device_id == tracker_entry.device_id
|
||||
assert sensor_entry.device_id is not None
|
||||
|
||||
device = device_registry.async_get(speed_entry.device_id)
|
||||
device = device_registry.async_get(sensor_entry.device_id)
|
||||
assert device is not None
|
||||
assert device.identifiers == {("scorpiontrack", "101_1")}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user