"""Tests for the Peblar integration services.""" from typing import Any from unittest.mock import MagicMock from peblar import ( PeblarAuthenticationError, PeblarConnectionError, PeblarError, PeblarRfidToken, PeblarVehicleToken, ) import pytest from homeassistant.components.peblar.const import DOMAIN from homeassistant.components.peblar.services import ( SERVICE_ADD_RFID_TOKEN, SERVICE_ADD_VEHICLE_TOKEN, SERVICE_DELETE_RFID_TOKEN, SERVICE_DELETE_VEHICLE_TOKEN, SERVICE_LIST_RFID_TOKENS, SERVICE_LIST_VEHICLE_TOKENS, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from tests.common import MockConfigEntry async def test_services_registered_on_setup( hass: HomeAssistant, init_integration: MockConfigEntry, ) -> None: """Test that RFID services are registered when entry is loaded.""" assert hass.services.has_service(DOMAIN, SERVICE_LIST_RFID_TOKENS) assert hass.services.has_service(DOMAIN, SERVICE_ADD_RFID_TOKEN) assert hass.services.has_service(DOMAIN, SERVICE_DELETE_RFID_TOKEN) async def test_services_survive_entry_unload( hass: HomeAssistant, init_integration: MockConfigEntry, ) -> None: """Test RFID services stay registered when the last Peblar entry unloads.""" await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() assert hass.services.has_service(DOMAIN, SERVICE_LIST_RFID_TOKENS) assert hass.services.has_service(DOMAIN, SERVICE_ADD_RFID_TOKEN) assert hass.services.has_service(DOMAIN, SERVICE_DELETE_RFID_TOKEN) async def test_list_rfid_tokens( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test list_rfid_tokens returns token list.""" mock_peblar.rfid_tokens.return_value = [ PeblarRfidToken( rfid_token_uid="AA:BB:CC:DD", rfid_token_description="My Card", ), PeblarRfidToken( rfid_token_uid="11:22:33:44", rfid_token_description="Work Badge", ), ] result = await hass.services.async_call( DOMAIN, SERVICE_LIST_RFID_TOKENS, {"config_entry_id": init_integration.entry_id}, blocking=True, return_response=True, ) assert result == { "tokens": [ {"uid": "AA:BB:CC:DD", "description": "My Card"}, {"uid": "11:22:33:44", "description": "Work Badge"}, ] } mock_peblar.rfid_tokens.assert_called_once_with() async def test_add_rfid_token( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test add_rfid_token calls library with correct args.""" await hass.services.async_call( DOMAIN, SERVICE_ADD_RFID_TOKEN, { "config_entry_id": init_integration.entry_id, "uid": "AA:BB:CC:DD", "description": "My Card", }, blocking=True, ) mock_peblar.add_rfid_token.assert_called_once_with( rfid_token_uid="AA:BB:CC:DD", rfid_token_description="My Card", ) async def test_delete_rfid_token( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test delete_rfid_token calls library with correct args.""" await hass.services.async_call( DOMAIN, SERVICE_DELETE_RFID_TOKEN, { "config_entry_id": init_integration.entry_id, "uid": "AA:BB:CC:DD", }, blocking=True, ) mock_peblar.delete_rfid_token.assert_called_once_with(uid="AA:BB:CC:DD") async def test_unloaded_config_entry_raises( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test service raises ServiceValidationError for an unloaded entry.""" second_entry = MockConfigEntry( domain=DOMAIN, data=mock_config_entry.data, unique_id="second-charger", ) second_entry.add_to_hass(hass) await hass.config_entries.async_setup(second_entry.entry_id) await hass.async_block_till_done() await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() with pytest.raises(ServiceValidationError) as excinfo: await hass.services.async_call( DOMAIN, SERVICE_LIST_RFID_TOKENS, {"config_entry_id": init_integration.entry_id}, blocking=True, return_response=True, ) assert excinfo.value.translation_key == "service_config_entry_not_loaded" SERVICE_CALLS: list[tuple[str, str, dict[str, Any]]] = [ (SERVICE_LIST_RFID_TOKENS, "rfid_tokens", {}), ( SERVICE_ADD_RFID_TOKEN, "add_rfid_token", {"uid": "AA:BB:CC:DD", "description": "My Card"}, ), (SERVICE_DELETE_RFID_TOKEN, "delete_rfid_token", {"uid": "AA:BB:CC:DD"}), ] @pytest.mark.parametrize(("service", "method_name", "service_data"), SERVICE_CALLS) @pytest.mark.parametrize( ("error", "translation_key"), [ (PeblarConnectionError("Could not connect"), "communication_error"), (PeblarError("Something went wrong"), "unknown_error"), ], ) async def test_service_communication_error( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, service: str, method_name: str, service_data: dict[str, Any], error: Exception, translation_key: str, ) -> None: """Test Peblar library errors are translated into Home Assistant errors.""" getattr(mock_peblar, method_name).side_effect = error with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( DOMAIN, service, {"config_entry_id": init_integration.entry_id, **service_data}, blocking=True, return_response=service == "list_rfid_tokens", ) assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == translation_key assert excinfo.value.translation_placeholders == {"error": str(error)} @pytest.mark.parametrize(("service", "method_name", "service_data"), SERVICE_CALLS) async def test_service_authentication_error( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, service: str, method_name: str, service_data: dict[str, Any], ) -> None: """Test an authentication error triggers a reauthentication flow.""" getattr(mock_peblar, method_name).side_effect = PeblarAuthenticationError( "Authentication error" ) mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error") with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( DOMAIN, service, {"config_entry_id": init_integration.entry_id, **service_data}, blocking=True, return_response=service == "list_rfid_tokens", ) assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "authentication_error" assert not excinfo.value.translation_placeholders await hass.async_block_till_done() assert init_integration.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["step_id"] == "reauth_confirm" assert flows[0]["context"].get("source") == SOURCE_REAUTH assert flows[0]["context"].get("entry_id") == init_integration.entry_id async def test_invalid_config_entry_raises( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test service raises ServiceValidationError for unknown entry ID.""" with pytest.raises(ServiceValidationError) as excinfo: await hass.services.async_call( DOMAIN, SERVICE_LIST_RFID_TOKENS, {"config_entry_id": "nonexistent-entry-id"}, blocking=True, return_response=True, ) assert excinfo.value.translation_key == "service_config_entry_not_found" @pytest.mark.parametrize(("service", "method_name", "service_data"), SERVICE_CALLS) @pytest.mark.parametrize("mock_peblar", [{"HwHasRfid": False}], indirect=True) async def test_charger_without_rfid_reader( hass: HomeAssistant, init_integration: MockConfigEntry, service: str, method_name: str, service_data: dict[str, Any], ) -> None: """A charger without a reader has no standalone list to manage.""" with pytest.raises(ServiceValidationError) as excinfo: await hass.services.async_call( DOMAIN, service, {"config_entry_id": init_integration.entry_id, **service_data}, blocking=True, return_response=service == SERVICE_LIST_RFID_TOKENS, ) assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "no_rfid_hardware" async def test_list_vehicle_tokens( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test list_vehicle_tokens returns the vehicles on the charger.""" mock_peblar.vehicle_tokens.return_value = [ PeblarVehicleToken(evcc_id="EVCC-1234", alias="The blue one"), PeblarVehicleToken(evcc_id="EVCC-5678", alias="The other one"), ] result = await hass.services.async_call( DOMAIN, SERVICE_LIST_VEHICLE_TOKENS, {"config_entry_id": init_integration.entry_id}, blocking=True, return_response=True, ) assert result == { "vehicles": [ {"evcc_id": "EVCC-1234", "alias": "The blue one"}, {"evcc_id": "EVCC-5678", "alias": "The other one"}, ] } mock_peblar.vehicle_tokens.assert_called_once_with() async def test_add_vehicle_token( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test add_vehicle_token calls the library with the right arguments.""" await hass.services.async_call( DOMAIN, SERVICE_ADD_VEHICLE_TOKEN, { "config_entry_id": init_integration.entry_id, "evcc_id": "EVCC-1234", "alias": "The blue one", }, blocking=True, ) mock_peblar.add_vehicle_token.assert_called_once_with( evcc_id="EVCC-1234", alias="The blue one", ) async def test_delete_vehicle_token( hass: HomeAssistant, mock_peblar: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test delete_vehicle_token calls the library with the right arguments.""" await hass.services.async_call( DOMAIN, SERVICE_DELETE_VEHICLE_TOKEN, { "config_entry_id": init_integration.entry_id, "evcc_id": "EVCC-1234", }, blocking=True, ) mock_peblar.delete_vehicle_token.assert_called_once_with(evcc_id="EVCC-1234") AUTOCHARGE_CALLS: list[tuple[str, dict[str, Any]]] = [ (SERVICE_LIST_VEHICLE_TOKENS, {}), (SERVICE_ADD_VEHICLE_TOKEN, {"evcc_id": "EVCC-1234", "alias": "The blue one"}), (SERVICE_DELETE_VEHICLE_TOKEN, {"evcc_id": "EVCC-1234"}), ] @pytest.mark.parametrize("mock_peblar", [{"HwHasRfid": False}], indirect=True) @pytest.mark.parametrize(("service", "service_data"), AUTOCHARGE_CALLS) @pytest.mark.usefixtures("mock_peblar") async def test_autocharge_does_not_need_an_rfid_reader( hass: HomeAssistant, init_integration: MockConfigEntry, service: str, service_data: dict[str, Any], ) -> None: """Test the autocharge list is a separate one from the RFID list. Autocharge identifies a car by what its own controller presents, so it has nothing to do with the reader. """ await hass.services.async_call( DOMAIN, service, {"config_entry_id": init_integration.entry_id, **service_data}, blocking=True, return_response=service == SERVICE_LIST_VEHICLE_TOKENS, ) @pytest.mark.parametrize("mock_peblar", [{"HwHasPlc": False}], indirect=True) @pytest.mark.parametrize(("service", "service_data"), AUTOCHARGE_CALLS) @pytest.mark.usefixtures("mock_peblar") async def test_autocharge_needs_power_line_communication( hass: HomeAssistant, init_integration: MockConfigEntry, service: str, service_data: dict[str, Any], ) -> None: """Test a charger that cannot do autocharge is turned away. Checked against a charger without the hardware: it answers 200 with null on the list, and 403 on adding and deleting. Letting that come back as a failed request tells the user nothing. """ with pytest.raises(ServiceValidationError) as excinfo: await hass.services.async_call( DOMAIN, service, {"config_entry_id": init_integration.entry_id, **service_data}, blocking=True, return_response=service == SERVICE_LIST_VEHICLE_TOKENS, ) assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "no_autocharge_hardware"