From 0efc9601fe302c400c778bdf1a59ca0c0d2b6e39 Mon Sep 17 00:00:00 2001 From: Anthony <210036686+Herbertmt978@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:29:11 +0100 Subject: [PATCH] Add ScorpionTrack ignition binary sensor (#177470) --- .../components/scorpiontrack/binary_sensor.py | 52 +++++++ .../components/scorpiontrack/const.py | 6 +- .../components/scorpiontrack/strings.json | 7 + .../snapshots/test_binary_sensor.ambr | 51 +++++++ .../scorpiontrack/test_binary_sensor.py | 133 ++++++++++++++++++ 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/scorpiontrack/binary_sensor.py create mode 100644 tests/components/scorpiontrack/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/scorpiontrack/test_binary_sensor.py diff --git a/homeassistant/components/scorpiontrack/binary_sensor.py b/homeassistant/components/scorpiontrack/binary_sensor.py new file mode 100644 index 000000000000..6ef69e78bea7 --- /dev/null +++ b/homeassistant/components/scorpiontrack/binary_sensor.py @@ -0,0 +1,52 @@ +"""Binary sensor platform for ScorpionTrack.""" + +from typing import override + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import ScorpionTrackConfigEntry, ScorpionTrackCoordinator +from .entity import ScorpionTrackEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ScorpionTrackConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up ScorpionTrack ignition binary sensors.""" + coordinator = entry.runtime_data + async_add_entities( + ScorpionTrackIgnitionBinarySensor(coordinator, vehicle.id) + for vehicle in coordinator.data.vehicles + ) + + +class ScorpionTrackIgnitionBinarySensor(ScorpionTrackEntity, BinarySensorEntity): + """Represent the latest shared vehicle ignition state.""" + + _attr_translation_key = "ignition" + + def __init__(self, coordinator: ScorpionTrackCoordinator, vehicle_id: int) -> None: + """Initialize the ignition binary sensor.""" + super().__init__(coordinator, vehicle_id) + self._attr_unique_id = f"{coordinator.data.id}_{vehicle_id}_ignition" + + def _available_ignition(self) -> bool | None: + """Return the ignition value.""" + return self.get_vehicle().position.ignition + + @property + @override + def available(self) -> bool: + """Return if the ignition binary sensor is available.""" + return super().available and self._available_ignition() is not None + + @property + @override + def is_on(self) -> bool | None: + """Return the ignition state.""" + return self._available_ignition() diff --git a/homeassistant/components/scorpiontrack/const.py b/homeassistant/components/scorpiontrack/const.py index 957318f878db..1748a8d096bc 100644 --- a/homeassistant/components/scorpiontrack/const.py +++ b/homeassistant/components/scorpiontrack/const.py @@ -12,4 +12,8 @@ CONF_SHARE_TOKEN = "share_token" DEFAULT_SCAN_INTERVAL = timedelta(minutes=2) -PLATFORMS: tuple[Platform, ...] = (Platform.DEVICE_TRACKER, Platform.SENSOR) +PLATFORMS: tuple[Platform, ...] = ( + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, +) diff --git a/homeassistant/components/scorpiontrack/strings.json b/homeassistant/components/scorpiontrack/strings.json index 63595a78ae09..e2315e4b60f8 100644 --- a/homeassistant/components/scorpiontrack/strings.json +++ b/homeassistant/components/scorpiontrack/strings.json @@ -22,6 +22,13 @@ } } }, + "entity": { + "binary_sensor": { + "ignition": { + "name": "Ignition" + } + } + }, "exceptions": { "cannot_connect": { "message": "The ScorpionTrack share could not be reached right now." diff --git a/tests/components/scorpiontrack/snapshots/test_binary_sensor.ambr b/tests/components/scorpiontrack/snapshots/test_binary_sensor.ambr new file mode 100644 index 000000000000..96048be2c995 --- /dev/null +++ b/tests/components/scorpiontrack/snapshots/test_binary_sensor.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_ignition_binary_sensor_snapshot[binary_sensor.ab12_cde_ignition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ab12_cde_ignition', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Ignition', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ignition', + 'platform': 'scorpiontrack', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ignition', + 'unique_id': '101_1_ignition', + 'unit_of_measurement': None, + }) +# --- +# name: test_ignition_binary_sensor_snapshot[binary_sensor.ab12_cde_ignition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'AB12 CDE Ignition', + }), + 'context': , + 'entity_id': 'binary_sensor.ab12_cde_ignition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/scorpiontrack/test_binary_sensor.py b/tests/components/scorpiontrack/test_binary_sensor.py new file mode 100644 index 000000000000..bfdc2e83610a --- /dev/null +++ b/tests/components/scorpiontrack/test_binary_sensor.py @@ -0,0 +1,133 @@ +"""Test the ScorpionTrack binary sensor platform.""" + +from dataclasses import replace +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pyscorpiontrack import ScorpionTrackShare +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.scorpiontrack.const import DEFAULT_SCAN_INTERVAL +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +ENTITY_ID = "binary_sensor.ab12_cde_ignition" + + +async def test_ignition_binary_sensor_state( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_scorpiontrack_client: AsyncMock, +) -> None: + """Test the ignition binary sensor uses the coordinator snapshot.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + assert ATTR_LATITUDE not in state.attributes + assert ATTR_LONGITUDE not in state.attributes + mock_scorpiontrack_client.async_get_share.assert_awaited_once_with() + + +async def test_ignition_binary_sensor_snapshot( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the ignition binary sensor entity and state attributes.""" + with patch( + "homeassistant.components.scorpiontrack.PLATFORMS", (Platform.BINARY_SENSOR,) + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("ignition", "expected_state"), + [ + pytest.param(True, STATE_ON, id="on"), + pytest.param(False, STATE_OFF, id="off"), + pytest.param(None, STATE_UNAVAILABLE, id="missing"), + ], +) +async def test_ignition_binary_sensor_availability( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_share: ScorpionTrackShare, + mock_scorpiontrack_client: AsyncMock, + ignition: bool | None, + expected_state: str, +) -> None: + """Test on, off, and missing ignition values.""" + vehicle = mock_share.vehicles[0] + mock_scorpiontrack_client.async_get_share.return_value = replace( + mock_share, + vehicles=( + replace( + vehicle, + position=replace(vehicle.position, ignition=ignition), + ), + ), + ) + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == expected_state + + +async def test_removed_vehicle_makes_ignition_binary_sensor_unavailable( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_share: ScorpionTrackShare, + mock_scorpiontrack_client: AsyncMock, +) -> None: + """Test ignition becomes unavailable if its vehicle leaves the share.""" + await setup_integration(hass, mock_config_entry) + + mock_scorpiontrack_client.async_get_share.return_value = replace( + mock_share, vehicles=() + ) + freezer.tick(DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_ignition_binary_sensor_uses_existing_vehicle_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test ignition shares the vehicle device with the tracker.""" + await setup_integration(hass, mock_config_entry) + + ignition_entry = entity_registry.async_get(ENTITY_ID) + tracker_entry = entity_registry.async_get("device_tracker.ab12_cde") + assert ignition_entry is not None + assert tracker_entry is not None + assert ignition_entry.unique_id == "101_1_ignition" + assert ignition_entry.original_device_class is None + assert ignition_entry.device_id == tracker_entry.device_id + assert ignition_entry.device_id is not None