From de4e1c444e87e89f61d27b9a0f99be266176cdb6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Apr 2026 12:00:13 -0400 Subject: [PATCH] Restrict homematic.set_install_mode service to admins (#169203) Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: balloob <1444314+balloob@users.noreply.github.com> --- .../components/homematic/__init__.py | 11 +++- tests/components/homematic/test_init.py | 64 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 tests/components/homematic/test_init.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 41d965fab11..4c64fdb5f42 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -26,7 +26,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType +from homeassistant.util.async_ import run_callback_threadsafe from .const import ( ATTR_ADDRESS, @@ -381,12 +383,15 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: homematic.setInstallMode(interface, t=time, mode=mode, address=address) - hass.services.register( + run_callback_threadsafe( + hass.loop, + async_register_admin_service, + hass, DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode, - schema=SCHEMA_SERVICE_SET_INSTALL_MODE, - ) + SCHEMA_SERVICE_SET_INSTALL_MODE, + ).result() def _service_put_paramset(service: ServiceCall) -> None: """Service to call the putParamset method on a HomeMatic connection.""" diff --git a/tests/components/homematic/test_init.py b/tests/components/homematic/test_init.py new file mode 100644 index 00000000000..f07eba864ee --- /dev/null +++ b/tests/components/homematic/test_init.py @@ -0,0 +1,64 @@ +"""Tests for the Homematic integration.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.homematic.const import ( + ATTR_INTERFACE, + DATA_HOMEMATIC, + SERVICE_SET_INSTALL_MODE, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import Unauthorized +from homeassistant.setup import async_setup_component + +from tests.common import MockUser + +DOMAIN = "homematic" +BASE_CONFIG = {DOMAIN: {"hosts": {"ccu2": {"host": "127.0.0.1"}}}} + + +@pytest.fixture +async def setup_homematic(hass: HomeAssistant) -> None: + """Set up the homematic component.""" + with patch( + "homeassistant.components.homematic.HMConnection", + return_value=MagicMock(), + ): + await async_setup_component(hass, DOMAIN, BASE_CONFIG) + await hass.async_block_till_done() + + +async def test_set_install_mode_admin_allowed( + hass: HomeAssistant, + setup_homematic: None, + hass_admin_user: MockUser, +) -> None: + """Test that admin users can call set_install_mode.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_INSTALL_MODE, + {ATTR_INTERFACE: "ccu2"}, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + hass.data[DATA_HOMEMATIC].setInstallMode.assert_called_once_with( + "ccu2", t=60, mode=1, address=None + ) + + +async def test_set_install_mode_non_admin_rejected( + hass: HomeAssistant, + setup_homematic: None, + hass_read_only_user: MockUser, +) -> None: + """Test that non-admin users cannot call set_install_mode.""" + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_INSTALL_MODE, + {ATTR_INTERFACE: "ccu2"}, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + )