diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 2b4d680208e..648368db6d0 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -408,6 +408,20 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity): if not alarm_code or code == alarm_code: return + current_context = ( + self._context if hasattr(self, "_context") and self._context else None + ) + user_id_from_context = current_context.user_id if current_context else None + + self.hass.bus.async_fire( + "manual_alarm_bad_code_attempt", + { + "entity_id": self.entity_id, + "user_id": user_id_from_context, + "target_state": state, + }, + ) + raise ServiceValidationError( "Invalid alarm code provided", translation_domain=DOMAIN, diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 941d7523220..fa9248e8f38 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -6,8 +6,10 @@ from unittest.mock import MagicMock, patch from freezegun import freeze_time import pytest +from homeassistant.auth.models import User from homeassistant.components import alarm_control_panel from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState, ) @@ -25,7 +27,7 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_VACATION, ) -from homeassistant.core import CoreState, HomeAssistant, State +from homeassistant.core import Context, CoreState, HomeAssistant, State, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -1123,6 +1125,80 @@ async def test_disarm_during_trigger_with_invalid_code(hass: HomeAssistant) -> N assert state.state == AlarmControlPanelState.TRIGGERED +async def test_bad_code_attempt_event_fired(hass: HomeAssistant) -> None: + """Test that manual_alarm_bad_code_attempt event is fired on bad code.""" + + entity_id = "alarm_control_panel.test_alarm" + config = { + ALARM_DOMAIN: { + "platform": "manual", + "name": "Test Alarm", + "code": "1234", + "delay_time": 0, + "arming_time": 0, + "trigger_time": 0, + } + } + assert await async_setup_component(hass, ALARM_DOMAIN, config) + await hass.async_block_till_done() + + alarm_entity = hass.states.get(entity_id) + assert alarm_entity is not None + + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_arm_away", + {"entity_id": entity_id, "code": "1234"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == AlarmControlPanelState.ARMED_AWAY + + bad_code = "0000" + + mock_user_id = "test_user_id_123" + test_context = Context(user_id=mock_user_id) + + events = [] + + @callback + def event_listener(event): + events.append(event.data) + + hass.bus.async_listen("manual_alarm_bad_code_attempt", event_listener) + + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_disarm", + {"entity_id": entity_id, "code": "1234"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(events) == 0 + + with patch("homeassistant.auth.AuthManager.async_get_user") as mock_get_user: + mock_user = MagicMock(spec=User) + mock_user.id = mock_user_id + mock_get_user.return_value = mock_user + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_disarm", + {"entity_id": entity_id, "code": bad_code}, + blocking=True, + context=test_context, + ) + + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].get("entity_id") == entity_id + assert events[0].get("target_state") == AlarmControlPanelState.DISARMED + assert events[0].get("user_id") == mock_user_id + + async def test_disarm_with_template_code(hass: HomeAssistant) -> None: """Attempt to disarm with a valid or invalid template-based code.""" assert await async_setup_component(