diff --git a/homeassistant/components/israel_rail/coordinator.py b/homeassistant/components/israel_rail/coordinator.py index 0621202dbcb..ae54834affd 100644 --- a/homeassistant/components/israel_rail/coordinator.py +++ b/homeassistant/components/israel_rail/coordinator.py @@ -79,6 +79,17 @@ class IsraelRailDataUpdateCoordinator(DataUpdateCoordinator[list[DataConnection] "Unable to connect and retrieve data from israelrail api", ) from e + offset = 0 + now = dt_util.now() + while offset < len(train_routes): + route = train_routes[offset] + if route is None: + break + route_departure = departure_time(route) + if route_departure is None or route_departure >= now: + break + offset += 1 + return [ DataConnection( departure=departure_time(train_routes[i]), @@ -89,6 +100,6 @@ class IsraelRailDataUpdateCoordinator(DataUpdateCoordinator[list[DataConnection] start=station_name_to_id(train_routes[i].trains[0].src), destination=station_name_to_id(train_routes[i].trains[-1].dst), ) - for i in range(DEPARTURES_COUNT) + for i in range(offset, offset + DEPARTURES_COUNT) if len(train_routes) > i and train_routes[i] is not None ] diff --git a/homeassistant/components/israel_rail/sensor.py b/homeassistant/components/israel_rail/sensor.py index 1cfdb97242e..3582695a928 100644 --- a/homeassistant/components/israel_rail/sensor.py +++ b/homeassistant/components/israel_rail/sensor.py @@ -52,30 +52,46 @@ DEPARTURE_SENSORS: tuple[IsraelRailSensorEntityDescription, ...] = ( ) SENSORS: tuple[IsraelRailSensorEntityDescription, ...] = ( - IsraelRailSensorEntityDescription( - key="platform", - translation_key="platform", - value_fn=lambda data_connection: data_connection.platform, - ), - IsraelRailSensorEntityDescription( - key="trains", - translation_key="trains", - value_fn=lambda data_connection: data_connection.trains, - ), - IsraelRailSensorEntityDescription( - key="train_number", - translation_key="train_number", - value_fn=lambda data_connection: data_connection.train_number, - ), - IsraelRailSensorEntityDescription( - key="departure_delay", - translation_key="departure_delay", - device_class=SensorDeviceClass.DURATION, - native_unit_of_measurement=UnitOfTime.MINUTES, - state_class=SensorStateClass.MEASUREMENT, - suggested_display_precision=0, - value_fn=lambda data_connection: data_connection.departure_delay, - ), + *[ + IsraelRailSensorEntityDescription( + key=f"platform{i or ''}", + translation_key=f"platform{i or ''}", + value_fn=lambda data_connection: data_connection.platform, + index=i, + ) + for i in range(DEPARTURES_COUNT) + ], + *[ + IsraelRailSensorEntityDescription( + key=f"trains{i or ''}", + translation_key=f"trains{i or ''}", + value_fn=lambda data_connection: data_connection.trains, + index=i, + ) + for i in range(DEPARTURES_COUNT) + ], + *[ + IsraelRailSensorEntityDescription( + key=f"train_number{i or ''}", + translation_key=f"train_number{i or ''}", + value_fn=lambda data_connection: data_connection.train_number, + index=i, + ) + for i in range(DEPARTURES_COUNT) + ], + *[ + IsraelRailSensorEntityDescription( + key=f"departure_delay{i or ''}", + translation_key=f"departure_delay{i or ''}", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + value_fn=lambda data_connection: data_connection.departure_delay, + index=i, + ) + for i in range(DEPARTURES_COUNT) + ], ) diff --git a/homeassistant/components/israel_rail/strings.json b/homeassistant/components/israel_rail/strings.json index e7380c80245..d4848be67b8 100644 --- a/homeassistant/components/israel_rail/strings.json +++ b/homeassistant/components/israel_rail/strings.json @@ -31,14 +31,38 @@ "departure_delay": { "name": "Departure delay" }, + "departure_delay1": { + "name": "Departure delay +1" + }, + "departure_delay2": { + "name": "Departure delay +2" + }, "platform": { "name": "Platform" }, + "platform1": { + "name": "Platform +1" + }, + "platform2": { + "name": "Platform +2" + }, "train_number": { "name": "Train number" }, + "train_number1": { + "name": "Train number +1" + }, + "train_number2": { + "name": "Train number +2" + }, "trains": { "name": "Trains" + }, + "trains1": { + "name": "Trains +1" + }, + "trains2": { + "name": "Trains +2" } } } diff --git a/tests/components/israel_rail/snapshots/test_sensor.ambr b/tests/components/israel_rail/snapshots/test_sensor.ambr index a094feae953..55fde8d8734 100644 --- a/tests/components/israel_rail/snapshots/test_sensor.ambr +++ b/tests/components/israel_rail/snapshots/test_sensor.ambr @@ -214,6 +214,124 @@ 'state': '0', }) # --- +# name: test_valid_config[sensor.mock_title_departure_delay_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_departure_delay_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Departure delay +1', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Departure delay +1', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'departure_delay1', + 'unique_id': 'באר יעקב אשקלון_departure_delay1', + 'unit_of_measurement': , + }) +# --- +# name: test_valid_config[sensor.mock_title_departure_delay_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'device_class': 'duration', + 'friendly_name': 'Mock Title Departure delay +1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_departure_delay_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_valid_config[sensor.mock_title_departure_delay_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_departure_delay_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Departure delay +2', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Departure delay +2', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'departure_delay2', + 'unique_id': 'באר יעקב אשקלון_departure_delay2', + 'unit_of_measurement': , + }) +# --- +# name: test_valid_config[sensor.mock_title_departure_delay_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'device_class': 'duration', + 'friendly_name': 'Mock Title Departure delay +2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_title_departure_delay_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_valid_config[sensor.mock_title_platform-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -265,6 +383,108 @@ 'state': '1', }) # --- +# name: test_valid_config[sensor.mock_title_platform_1-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': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_platform_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Platform +1', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Platform +1', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'platform1', + 'unique_id': 'באר יעקב אשקלון_platform1', + 'unit_of_measurement': None, + }) +# --- +# name: test_valid_config[sensor.mock_title_platform_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'friendly_name': 'Mock Title Platform +1', + }), + 'context': , + 'entity_id': 'sensor.mock_title_platform_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_valid_config[sensor.mock_title_platform_2-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': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_platform_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Platform +2', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Platform +2', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'platform2', + 'unique_id': 'באר יעקב אשקלון_platform2', + 'unit_of_measurement': None, + }) +# --- +# name: test_valid_config[sensor.mock_title_platform_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'friendly_name': 'Mock Title Platform +2', + }), + 'context': , + 'entity_id': 'sensor.mock_title_platform_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- # name: test_valid_config[sensor.mock_title_train_number-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -316,6 +536,108 @@ 'state': '1234', }) # --- +# name: test_valid_config[sensor.mock_title_train_number_1-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': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_train_number_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Train number +1', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Train number +1', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'train_number1', + 'unique_id': 'באר יעקב אשקלון_train_number1', + 'unit_of_measurement': None, + }) +# --- +# name: test_valid_config[sensor.mock_title_train_number_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'friendly_name': 'Mock Title Train number +1', + }), + 'context': , + 'entity_id': 'sensor.mock_title_train_number_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1235', + }) +# --- +# name: test_valid_config[sensor.mock_title_train_number_2-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': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_train_number_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Train number +2', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Train number +2', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'train_number2', + 'unique_id': 'באר יעקב אשקלון_train_number2', + 'unit_of_measurement': None, + }) +# --- +# name: test_valid_config[sensor.mock_title_train_number_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'friendly_name': 'Mock Title Train number +2', + }), + 'context': , + 'entity_id': 'sensor.mock_title_train_number_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1236', + }) +# --- # name: test_valid_config[sensor.mock_title_trains-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -367,3 +689,105 @@ 'state': '1', }) # --- +# name: test_valid_config[sensor.mock_title_trains_1-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': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_trains_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Trains +1', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Trains +1', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'trains1', + 'unique_id': 'באר יעקב אשקלון_trains1', + 'unit_of_measurement': None, + }) +# --- +# name: test_valid_config[sensor.mock_title_trains_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'friendly_name': 'Mock Title Trains +1', + }), + 'context': , + 'entity_id': 'sensor.mock_title_trains_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_valid_config[sensor.mock_title_trains_2-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': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_title_trains_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Trains +2', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Trains +2', + 'platform': 'israel_rail', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'trains2', + 'unique_id': 'באר יעקב אשקלון_trains2', + 'unit_of_measurement': None, + }) +# --- +# name: test_valid_config[sensor.mock_title_trains_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Israel rail.', + 'friendly_name': 'Mock Title Trains +2', + }), + 'context': , + 'entity_id': 'sensor.mock_title_trains_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- diff --git a/tests/components/israel_rail/test_sensor.py b/tests/components/israel_rail/test_sensor.py index 56460cb3b4e..fed0d303da3 100644 --- a/tests/components/israel_rail/test_sensor.py +++ b/tests/components/israel_rail/test_sensor.py @@ -3,8 +3,10 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.israel_rail.const import DEPARTURES_COUNT from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -12,7 +14,20 @@ from homeassistant.helpers import entity_registry as er from . import goto_future, init_integration from .conftest import TRAINS, get_time, get_train_route -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +# A moment just before the first train in TRAINS departs (10:10 UTC), so the +# coordinator considers the head of the list as an upcoming departure. +BEFORE_FIRST_TRAIN = "2021-10-10T10:00:00+00:00" + +EXPECTED_ENTITY_COUNT = DEPARTURES_COUNT * 5 + + +@pytest.fixture(autouse=True) +def freeze_before_first_train(freezer: FrozenDateTimeFactory) -> FrozenDateTimeFactory: + """Freeze time before any train in TRAINS departs.""" + freezer.move_to(BEFORE_FIRST_TRAIN) + return freezer async def test_valid_config( @@ -29,22 +44,23 @@ async def test_valid_config( async def test_update_train( hass: HomeAssistant, - freezer: FrozenDateTimeFactory, + freeze_before_first_train: FrozenDateTimeFactory, mock_israelrail: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Ensure the train data is updated.""" await init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == 7 + assert len(hass.states.async_entity_ids()) == EXPECTED_ENTITY_COUNT departure_sensor = hass.states.get("sensor.mock_title_departure") expected_time = get_time(10, 10) assert departure_sensor.state == expected_time mock_israelrail.query.return_value = TRAINS[1:] + freeze_before_first_train.move_to("2021-10-10T10:15:00+00:00") + async_fire_time_changed(hass) + await hass.async_block_till_done() - await goto_future(hass, freezer) - - assert len(hass.states.async_entity_ids()) == 7 + assert len(hass.states.async_entity_ids()) == EXPECTED_ENTITY_COUNT departure_sensor = hass.states.get("sensor.mock_title_departure") expected_time = get_time(10, 20) assert departure_sensor.state == expected_time @@ -52,37 +68,37 @@ async def test_update_train( async def test_fail_query( hass: HomeAssistant, - freezer: FrozenDateTimeFactory, + freeze_before_first_train: FrozenDateTimeFactory, mock_israelrail: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Ensure the integration handles query failures.""" await init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == 7 + assert len(hass.states.async_entity_ids()) == EXPECTED_ENTITY_COUNT mock_israelrail.query.side_effect = Exception("error") - await goto_future(hass, freezer) - assert len(hass.states.async_entity_ids()) == 7 + await goto_future(hass, freeze_before_first_train) + assert len(hass.states.async_entity_ids()) == EXPECTED_ENTITY_COUNT departure_sensor = hass.states.get("sensor.mock_title_departure") assert departure_sensor.state == STATE_UNAVAILABLE async def test_no_departures( hass: HomeAssistant, - freezer: FrozenDateTimeFactory, + freeze_before_first_train: FrozenDateTimeFactory, mock_israelrail: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test handling when there are no departures available.""" await init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == 7 + assert len(hass.states.async_entity_ids()) == EXPECTED_ENTITY_COUNT # Simulate no departures (e.g., after-hours) mock_israelrail.query.return_value = [] - await goto_future(hass, freezer) + await goto_future(hass, freeze_before_first_train) # All sensors should still exist - assert len(hass.states.async_entity_ids()) == 7 + assert len(hass.states.async_entity_ids()) == EXPECTED_ENTITY_COUNT # Departure sensors should have unknown state (None) departure_sensor = hass.states.get("sensor.mock_title_departure") @@ -111,7 +127,7 @@ async def test_no_departures( async def test_departure_delay( hass: HomeAssistant, - freezer: FrozenDateTimeFactory, + freeze_before_first_train: FrozenDateTimeFactory, mock_israelrail: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: @@ -132,7 +148,104 @@ async def test_departure_delay( *TRAINS[1:], ] - await goto_future(hass, freezer) + # Refresh while still before TRAINS[0] departs, so the delay-bearing + # first route is treated as upcoming and not skipped. + freeze_before_first_train.move_to("2021-10-10T10:05:00+00:00") + async_fire_time_changed(hass) + await hass.async_block_till_done() departure_delay_sensor = hass.states.get("sensor.mock_title_departure_delay") assert departure_delay_sensor.state == "7" + + +async def test_skip_first_route_when_in_past( + hass: HomeAssistant, + freeze_before_first_train: FrozenDateTimeFactory, + mock_israelrail: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """When the first route already departed, sensors should reflect the next ones.""" + # Freeze "now" between TRAINS[0] (10:10) and TRAINS[1] (10:20) so the head + # of the list is in the past. Coordinator should shift the window by 1. + freeze_before_first_train.move_to("2021-10-10T10:15:00+00:00") + + await init_integration(hass, mock_config_entry) + + # The +0/+1/+2 departure sensors should now show TRAINS[1], TRAINS[2], TRAINS[3]. + assert hass.states.get("sensor.mock_title_departure").state == get_time(10, 20) + assert hass.states.get("sensor.mock_title_departure_1").state == get_time(10, 30) + assert hass.states.get("sensor.mock_title_departure_2").state == get_time(10, 40) + # The per-index sensors should also follow the shifted window — index 0 + # now points at the second route in the API response, index 1 at the third, etc. + assert hass.states.get("sensor.mock_title_train_number").state == "1235" + assert hass.states.get("sensor.mock_title_train_number_1").state == "1236" + assert hass.states.get("sensor.mock_title_train_number_2").state == "1237" + + +async def test_keep_first_route_when_upcoming( + hass: HomeAssistant, + freeze_before_first_train: FrozenDateTimeFactory, + mock_israelrail: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """When the first route is still upcoming, sensors should keep it.""" + # "now" sits well before TRAINS[0] (10:10), so no offset is applied. + await init_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.mock_title_departure").state == get_time(10, 10) + assert hass.states.get("sensor.mock_title_departure_1").state == get_time(10, 20) + assert hass.states.get("sensor.mock_title_departure_2").state == get_time(10, 30) + assert hass.states.get("sensor.mock_title_train_number").state == "1234" + + +async def test_skip_first_route_with_fewer_results( + hass: HomeAssistant, + freeze_before_first_train: FrozenDateTimeFactory, + mock_israelrail: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """A short result list with a past head still yields entries from the tail.""" + # Only two routes; the first is already in the past so only one remains. + mock_israelrail.query.return_value = [TRAINS[0], TRAINS[1]] + freeze_before_first_train.move_to("2021-10-10T10:15:00+00:00") + + await init_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.mock_title_departure").state == get_time(10, 20) + # No second/third routes available after the shift. + assert hass.states.get("sensor.mock_title_departure_1").state == STATE_UNKNOWN + assert hass.states.get("sensor.mock_title_departure_2").state == STATE_UNKNOWN + + +async def test_skip_multiple_past_routes( + hass: HomeAssistant, + freeze_before_first_train: FrozenDateTimeFactory, + mock_israelrail: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """When several routes at the head have already departed, the window shifts past them all.""" + # Freeze "now" past TRAINS[0] (10:10) and TRAINS[1] (10:20) but before TRAINS[2] (10:30). + freeze_before_first_train.move_to("2021-10-10T10:25:00+00:00") + + await init_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.mock_title_departure").state == get_time(10, 30) + assert hass.states.get("sensor.mock_title_departure_1").state == get_time(10, 40) + assert hass.states.get("sensor.mock_title_departure_2").state == get_time(10, 50) + assert hass.states.get("sensor.mock_title_train_number").state == "1236" + + +async def test_all_routes_in_past( + hass: HomeAssistant, + freeze_before_first_train: FrozenDateTimeFactory, + mock_israelrail: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """When every returned route has already departed, every departure sensor is unknown.""" + freeze_before_first_train.move_to("2021-10-10T11:00:00+00:00") + + await init_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.mock_title_departure").state == STATE_UNKNOWN + assert hass.states.get("sensor.mock_title_departure_1").state == STATE_UNKNOWN + assert hass.states.get("sensor.mock_title_departure_2").state == STATE_UNKNOWN