Repository: home-assistant/core
Pull request: Rework velbus services to deprecated the interface parameter
## Proposed change
<!--
  Describe the big picture of your changes here to communicate to the
  maintainers why we should accept this pull request. If it fixes a bug
  or resolves a feature request, be sure to link to that issue in the
  additional information section.
-->

Currently the velbus services use a interface parameter, this is a simple string that should be exactly the same as the config_entry parameter.

With this PR i'm deprecating this interface parameter and adding a new parameter that uses a config_entry selector, this is more user-friendly.

The interface parameter is deprecated and will be remover in the 2025.9 release.

## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [ ] New feature (which adds functionality to an existing integration)
- [X] Deprecation (breaking change to happen in the future)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [x] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #134855
- This PR is related to issue: 
- Link to documentation pull request: https://github.com/home-assistant/home-assistant.io/pull/36855

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [X] The code change is tested and works locally.
- [X] Local tests pass. **Your PR cannot be merged unless tests pass**
- [X] There is no commented out code in this PR.
- [X] I have followed the [development checklist][dev-checklist]
- [X] I have followed the [perfect PR recommendations][perfect-pr]
- [X] The code has been formatted using Ruff (`ruff format homeassistant tests`)
- [X] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [ ] The [manifest file][manifest-docs] has all fields filled out correctly.  
      Updated and included derived files by running: `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt`.  
      Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr


Problem statement:
[Velbus] Set_memo_text not working in HA 2025.1
### The problem

set_memo_text does not display any text anymore on module VMBELO since HA2025.1

### What version of Home Assistant Core has the issue?

2025.1.0

### What was the last working version of Home Assistant Core?

2024.12.5

### What type of installation are you running?

Home Assistant OS

### Integration causing the issue

Velbus

### Link to integration documentation on our website

_No response_

### Diagnostics information

no text displayed on module, no errors in log file.
All other Velbus items/actions are OK (controlling relays, lights, sensors)


### Example YAML snippet

```yaml
action: velbus.set_memo_text
data:
  interface: 192.168.2.17:3788
  memo_text: test text
  address: 115
```


### Anything in the logs that might be useful for us?

```txt
2025-01-06 08:40:16.056 INFO (MainThread) [homeassistant.helpers.script.websocket_api_script] websocket_api script: Running websocket_api script
2025-01-06 08:40:16.056 INFO (MainThread) [homeassistant.helpers.script.websocket_api_script] websocket_api script: Executing step call service
```


### Additional information

_No response_

Hints from issue discussion:

Commit message: Rework velbus services to deprecated the interface parameter

Patch to review:
diff --git a/homeassistant/components/velbus/const.py b/homeassistant/components/velbus/const.py
index 2d9f6e98a4cd7..b40f64e860793 100644
--- a/homeassistant/components/velbus/const.py
+++ b/homeassistant/components/velbus/const.py
@@ -11,6 +11,7 @@
 
 DOMAIN: Final = "velbus"
 
+CONF_CONFIG_ENTRY: Final = "config_entry"
 CONF_INTERFACE: Final = "interface"
 CONF_MEMO_TEXT: Final = "memo_text"
 
diff --git a/homeassistant/components/velbus/services.py b/homeassistant/components/velbus/services.py
index 3f0b1bd6cdb92..906ab42d41d8a 100644
--- a/homeassistant/components/velbus/services.py
+++ b/homeassistant/components/velbus/services.py
@@ -11,13 +11,16 @@
 
 from homeassistant.const import CONF_ADDRESS
 from homeassistant.core import HomeAssistant, ServiceCall
-from homeassistant.helpers import config_validation as cv
+from homeassistant.exceptions import ServiceValidationError
+from homeassistant.helpers import config_validation as cv, selector
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.helpers.storage import STORAGE_DIR
 
 if TYPE_CHECKING:
     from . import VelbusConfigEntry
 
 from .const import (
+    CONF_CONFIG_ENTRY,
     CONF_INTERFACE,
     CONF_MEMO_TEXT,
     DOMAIN,
@@ -32,6 +35,7 @@ def setup_services(hass: HomeAssistant) -> None:
     """Register the velbus services."""
 
     def check_entry_id(interface: str) -> str:
+        """Check the config_entry for a specific interface."""
         for config_entry in hass.config_entries.async_entries(DOMAIN):
             if "port" in config_entry.data and config_entry.data["port"] == interface:
                 return config_entry.entry_id
@@ -39,51 +43,68 @@ def check_entry_id(interface: str) -> str:
             "The interface provided is not defined as a port in a Velbus integration"
         )
 
-    def get_config_entry(interface: str) -> VelbusConfigEntry | None:
-        for config_entry in hass.config_entries.async_entries(DOMAIN):
-            if "port" in config_entry.data and config_entry.data["port"] == interface:
-                return config_entry
-        return None
+    async def get_config_entry(call: ServiceCall) -> VelbusConfigEntry:
+        """Get the config entry for this service call."""
+        if CONF_CONFIG_ENTRY in call.data:
+            entry_id = call.data[CONF_CONFIG_ENTRY]
+        elif CONF_INTERFACE in call.data:
+            # Deprecated in 2025.2, to remove in 2025.9
+            async_create_issue(
+                hass,
+                DOMAIN,
+                "deprecated_interface_parameter",
+                breaks_in_ha_version="2025.9.0",
+                is_fixable=False,
+                severity=IssueSeverity.WARNING,
+                translation_key="deprecated_interface_parameter",
+            )
+            entry_id = call.data[CONF_INTERFACE]
+        else:
+            entry_id = None
+        if entry_id is None:
+            raise ServiceValidationError(
+                "Either config entry or interface must be provided"
+            )
+        entry = hass.config_entries.async_get_entry(entry_id)
+        if entry and entry.domain == DOMAIN:
+            return entry
+        raise ServiceValidationError("Config entry not found")
 
     async def scan(call: ServiceCall) -> None:
         """Handle a scan service call."""
-        entry = get_config_entry(call.data[CONF_INTERFACE])
-        if entry:
-            await entry.runtime_data.controller.scan()
+        entry = await get_config_entry(call)
+        await entry.runtime_data.controller.scan()
 
     async def syn_clock(call: ServiceCall) -> None:
         """Handle a sync clock service call."""
-        entry = get_config_entry(call.data[CONF_INTERFACE])
-        if entry:
-            await entry.runtime_data.controller.sync_clock()
+        entry = await get_config_entry(call)
+        await entry.runtime_data.controller.sync_clock()
 
     async def set_memo_text(call: ServiceCall) -> None:
         """Handle Memo Text service call."""
-        entry = get_config_entry(call.data[CONF_INTERFACE])
-        if entry:
-            memo_text = call.data[CONF_MEMO_TEXT]
-            module = entry.runtime_data.controller.get_module(call.data[CONF_ADDRESS])
-            if module:
-                await module.set_memo_text(memo_text.async_render())
+        entry = await get_config_entry(call)
+        memo_text = call.data[CONF_MEMO_TEXT]
+        module = entry.runtime_data.controller.get_module(call.data[CONF_ADDRESS])
+        if not module:
+            raise ServiceValidationError("Module not found")
+        await module.set_memo_text(memo_text.async_render())
 
     async def clear_cache(call: ServiceCall) -> None:
         """Handle a clear cache service call."""
-        # clear the cache
+        entry = await get_config_entry(call)
         with suppress(FileNotFoundError):
             if call.data.get(CONF_ADDRESS):
                 await hass.async_add_executor_job(
                     os.unlink,
                     hass.config.path(
                         STORAGE_DIR,
-                        f"velbuscache-{call.data[CONF_INTERFACE]}/{call.data[CONF_ADDRESS]}.p",
+                        f"velbuscache-{entry.entry_id}/{call.data[CONF_ADDRESS]}.p",
                     ),
                 )
             else:
                 await hass.async_add_executor_job(
                     shutil.rmtree,
-                    hass.config.path(
-                        STORAGE_DIR, f"velbuscache-{call.data[CONF_INTERFACE]}/"
-                    ),
+                    hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}/"),
                 )
         # call a scan to repopulate
         await scan(call)
@@ -92,14 +113,32 @@ async def clear_cache(call: ServiceCall) -> None:
         DOMAIN,
         SERVICE_SCAN,
         scan,
-        vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}),
+        vol.Schema(
+            {
+                vol.Optional(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
+                vol.Optional(CONF_CONFIG_ENTRY): selector.ConfigEntrySelector(
+                    {
+                        "integration": DOMAIN,
+                    }
+                ),
+            }
+        ),
     )
 
     hass.services.async_register(
         DOMAIN,
         SERVICE_SYNC,
         syn_clock,
-        vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}),
+        vol.Schema(
+            {
+                vol.Optional(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
+                vol.Optional(CONF_CONFIG_ENTRY): selector.ConfigEntrySelector(
+                    {
+                        "integration": DOMAIN,
+                    }
+                ),
+            }
+        ),
     )
 
     hass.services.async_register(
@@ -108,7 +147,12 @@ async def clear_cache(call: ServiceCall) -> None:
         set_memo_text,
         vol.Schema(
             {
-                vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
+                vol.Optional(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
+                vol.Optional(CONF_CONFIG_ENTRY): selector.ConfigEntrySelector(
+                    {
+                        "integration": DOMAIN,
+                    }
+                ),
                 vol.Required(CONF_ADDRESS): vol.All(
                     vol.Coerce(int), vol.Range(min=0, max=255)
                 ),
@@ -123,7 +167,12 @@ async def clear_cache(call: ServiceCall) -> None:
         clear_cache,
         vol.Schema(
             {
-                vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
+                vol.Optional(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
+                vol.Optional(CONF_CONFIG_ENTRY): selector.ConfigEntrySelector(
+                    {
+                        "integration": DOMAIN,
+                    }
+                ),
                 vol.Optional(CONF_ADDRESS): vol.All(
                     vol.Coerce(int), vol.Range(min=0, max=255)
                 ),
diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml
index e3ecc3556f01b..398869136923d 100644
--- a/homeassistant/components/velbus/services.yaml
+++ b/homeassistant/components/velbus/services.yaml
@@ -1,29 +1,38 @@
 sync_clock:
   fields:
     interface:
-      required: true
       example: "192.168.1.5:27015"
       default: ""
       selector:
         text:
+    config_entry:
+      selector:
+        config_entry:
+          integration: velbus
 
 scan:
   fields:
     interface:
-      required: true
       example: "192.168.1.5:27015"
       default: ""
       selector:
         text:
+    config_entry:
+      selector:
+        config_entry:
+          integration: velbus
 
 clear_cache:
   fields:
     interface:
-      required: true
       example: "192.168.1.5:27015"
       default: ""
       selector:
         text:
+    config_entry:
+      selector:
+        config_entry:
+          integration: velbus
     address:
       required: false
       selector:
@@ -34,11 +43,14 @@ clear_cache:
 set_memo_text:
   fields:
     interface:
-      required: true
       example: "192.168.1.5:27015"
       default: ""
       selector:
         text:
+    config_entry:
+      selector:
+        config_entry:
+          integration: velbus
     address:
       required: true
       selector:
diff --git a/homeassistant/components/velbus/strings.json b/homeassistant/components/velbus/strings.json
index be1d992056eee..8bb078df6bd4e 100644
--- a/homeassistant/components/velbus/strings.json
+++ b/homeassistant/components/velbus/strings.json
@@ -30,6 +30,10 @@
         "interface": {
           "name": "Interface",
           "description": "The velbus interface to send the command to, this will be the same value as used during configuration."
+        },
+        "config_entry": {
+          "name": "Config entry",
+          "description": "The config entry of the velbus integration"
         }
       }
     },
@@ -40,6 +44,10 @@
         "interface": {
           "name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]",
           "description": "[%key:component::velbus::services::sync_clock::fields::interface::description%]"
+        },
+        "config_entry": {
+          "name": "[%key:component::velbus::services::sync_clock::fields::config_entry::name%]",
+          "description": "[%key:component::velbus::services::sync_clock::fields::config_entry::description%]"
         }
       }
     },
@@ -51,6 +59,10 @@
           "name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]",
           "description": "[%key:component::velbus::services::sync_clock::fields::interface::description%]"
         },
+        "config_entry": {
+          "name": "[%key:component::velbus::services::sync_clock::fields::config_entry::name%]",
+          "description": "[%key:component::velbus::services::sync_clock::fields::config_entry::description%]"
+        },
         "address": {
           "name": "Address",
           "description": "The module address in decimal format, if this is provided we only clear this module, if nothing is provided we clear the whole cache directory (all modules) The decimal addresses are displayed in front of the modules listed at the integration page."
@@ -65,6 +77,10 @@
           "name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]",
           "description": "[%key:component::velbus::services::sync_clock::fields::interface::description%]"
         },
+        "config_entry": {
+          "name": "[%key:component::velbus::services::sync_clock::fields::config_entry::name%]",
+          "description": "[%key:component::velbus::services::sync_clock::fields::config_entry::description%]"
+        },
         "address": {
           "name": "Address",
           "description": "The module address in decimal format. The decimal addresses are displayed in front of the modules listed at the integration page."
@@ -75,5 +91,11 @@
         }
       }
     }
+  },
+  "issues": {
+    "deprecated_interface_parameter": {
+      "title": "Deprecated 'interface' parameter",
+      "description": "The 'interface' parameter in the Velbus service calls is deprecated. The 'config_entry' parameter should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue."
+    }
   }
 }
diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py
index 95f691b34f87b..20d26a895c078 100644
--- a/tests/components/velbus/conftest.py
+++ b/tests/components/velbus/conftest.py
@@ -1,7 +1,7 @@
 """Fixtures for the Velbus tests."""
 
 from collections.abc import Generator
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, patch
 
 import pytest
 from velbusaio.channels import (
@@ -72,6 +72,7 @@ def mock_controller(
             4: mock_module_no_subdevices,
             99: mock_module_subdevices,
         }
+        cont.get_module.return_value = mock_module_subdevices
         yield controller
 
 
@@ -300,7 +301,7 @@ def mock_cover_no_position() -> AsyncMock:
 @pytest.fixture(name="config_entry")
 async def mock_config_entry(
     hass: HomeAssistant,
-    controller: MagicMock,
+    controller: AsyncMock,
 ) -> VelbusConfigEntry:
     """Create and register mock config entry."""
     config_entry = MockConfigEntry(
diff --git a/tests/components/velbus/test_services.py b/tests/components/velbus/test_services.py
new file mode 100644
index 0000000000000..fbb117b8d308c
--- /dev/null
+++ b/tests/components/velbus/test_services.py
@@ -0,0 +1,169 @@
+"""Velbus services tests."""
+
+from unittest.mock import AsyncMock
+
+import pytest
+import voluptuous as vol
+
+from homeassistant.components.velbus.const import (
+    CONF_CONFIG_ENTRY,
+    CONF_INTERFACE,
+    CONF_MEMO_TEXT,
+    DOMAIN,
+    SERVICE_CLEAR_CACHE,
+    SERVICE_SCAN,
+    SERVICE_SET_MEMO_TEXT,
+    SERVICE_SYNC,
+)
+from homeassistant.const import CONF_ADDRESS
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ServiceValidationError
+
+from . import init_integration
+
+from tests.common import MockConfigEntry
+
+
+async def test_global_services_with_interface(
+    hass: HomeAssistant,
+    config_entry: MockConfigEntry,
+) -> None:
+    """Test services directed at the bus with an interface parameter."""
+    await init_integration(hass, config_entry)
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_SCAN,
+        {CONF_INTERFACE: config_entry.data["port"]},
+        blocking=True,
+    )
+    config_entry.runtime_data.controller.scan.assert_called_once_with()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_SYNC,
+        {CONF_INTERFACE: config_entry.data["port"]},
+        blocking=True,
+    )
+    config_entry.runtime_data.controller.sync_clock.assert_called_once_with()
+
+    # Test invalid interface
+    with pytest.raises(vol.error.MultipleInvalid):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_SCAN,
+            {CONF_INTERFACE: "nonexistent"},
+            blocking=True,
+        )
+
+    # Test missing interface
+    with pytest.raises(ServiceValidationError):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_SCAN,
+            {},
+            blocking=True,
+        )
+
+
+async def test_global_survices_with_config_entry(
+    hass: HomeAssistant,
+    config_entry: MockConfigEntry,
+) -> None:
+    """Test services directed at the bus with a config_entry."""
+    await init_integration(hass, config_entry)
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_SCAN,
+        {CONF_CONFIG_ENTRY: config_entry.entry_id},
+        blocking=True,
+    )
+    config_entry.runtime_data.controller.scan.assert_called_once_with()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_SYNC,
+        {CONF_CONFIG_ENTRY: config_entry.entry_id},
+        blocking=True,
+    )
+    config_entry.runtime_data.controller.sync_clock.assert_called_once_with()
+
+    # Test invalid interface
+    with pytest.raises(ServiceValidationError):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_SCAN,
+            {CONF_CONFIG_ENTRY: "nonexistent"},
+            blocking=True,
+        )
+
+    # Test missing interface
+    with pytest.raises(ServiceValidationError):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_SCAN,
+            {},
+            blocking=True,
+        )
+
+
+async def test_set_memo_text(
+    hass: HomeAssistant,
+    config_entry: MockConfigEntry,
+    controller: AsyncMock,
+) -> None:
+    """Test the set_memo_text service."""
+    await init_integration(hass, config_entry)
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_SET_MEMO_TEXT,
+        {
+            CONF_CONFIG_ENTRY: config_entry.entry_id,
+            CONF_MEMO_TEXT: "Test",
+            CONF_ADDRESS: 1,
+        },
+        blocking=True,
+    )
+    config_entry.runtime_data.controller.get_module(
+        1
+    ).set_memo_text.assert_called_once_with("Test")
+
+    # Test with unfound module
+    controller.return_value.get_module.return_value = None
+    with pytest.raises(ServiceValidationError):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_SET_MEMO_TEXT,
+            {
+                CONF_CONFIG_ENTRY: config_entry.entry_id,
+                CONF_MEMO_TEXT: "Test",
+                CONF_ADDRESS: 2,
+            },
+            blocking=True,
+        )
+
+
+async def test_clear_cache(
+    hass: HomeAssistant,
+    config_entry: MockConfigEntry,
+) -> None:
+    """Test the clear_cache service."""
+    await init_integration(hass, config_entry)
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_CLEAR_CACHE,
+        {CONF_CONFIG_ENTRY: config_entry.entry_id},
+        blocking=True,
+    )
+    config_entry.runtime_data.controller.scan.assert_called_once_with()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_CLEAR_CACHE,
+        {CONF_CONFIG_ENTRY: config_entry.entry_id, CONF_ADDRESS: 1},
+        blocking=True,
+    )
+    assert config_entry.runtime_data.controller.scan.call_count == 2


Code review comments:
diff_hunk: @@ -32,58 +35,76 @@ def setup_services(hass: HomeAssistant) -> None:
     """Register the velbus services."""
 
     def check_entry_id(interface: str) -> str:
+        """Check the config_entry for a specific interface."""
         for config_entry in hass.config_entries.async_entries(DOMAIN):
             if "port" in config_entry.data and config_entry.data["port"] == interface:
                 return config_entry.entry_id
         raise vol.Invalid(
             "The interface provided is not defined as a port in a Velbus integration"
         )
 
-    def get_config_entry(interface: str) -> VelbusConfigEntry | None:
-        for config_entry in hass.config_entries.async_entries(DOMAIN):
-            if "port" in config_entry.data and config_entry.data["port"] == interface:
-                return config_entry
-        return None
+    async def get_config_entry(call: ServiceCall) -> VelbusConfigEntry:
+        """Get the config entry for this service call."""
+        if CONF_CONFIG_ENTRY in call.data:
+            entry_id = call.data[CONF_CONFIG_ENTRY]
+        elif CONF_INTERFACE in call.data:
+            # Deprecated in 2025.2, to remove in 2025.9
path: homeassistant/components/velbus/services.py
text: @user1:
Can be 2025.8, 6 months if this is released in 2025.2

diff_hunk: @@ -32,58 +35,76 @@ def setup_services(hass: HomeAssistant) -> None:
     """Register the velbus services."""
 
     def check_entry_id(interface: str) -> str:
+        """Check the config_entry for a specific interface."""
         for config_entry in hass.config_entries.async_entries(DOMAIN):
             if "port" in config_entry.data and config_entry.data["port"] == interface:
                 return config_entry.entry_id
         raise vol.Invalid(
             "The interface provided is not defined as a port in a Velbus integration"
         )
 
-    def get_config_entry(interface: str) -> VelbusConfigEntry | None:
-        for config_entry in hass.config_entries.async_entries(DOMAIN):
-            if "port" in config_entry.data and config_entry.data["port"] == interface:
-                return config_entry
-        return None
+    async def get_config_entry(call: ServiceCall) -> VelbusConfigEntry:
+        """Get the config entry for this service call."""
+        if CONF_CONFIG_ENTRY in call.data:
+            entry_id = call.data[CONF_CONFIG_ENTRY]
+        elif CONF_INTERFACE in call.data:
+            # Deprecated in 2025.2, to remove in 2025.9
+            async_create_issue(
+                hass,
+                DOMAIN,
+                "deprecated_interface_parameter",
+                breaks_in_ha_version="2025.9.0",
+                is_fixable=False,
+                severity=IssueSeverity.WARNING,
+                translation_key="deprecated_interface_parameter",
+            )
+            entry_id = call.data[CONF_INTERFACE]
+        else:
+            entry_id = None
+        if entry_id is None:
+            raise ServiceValidationError(
+                "Either config entry or interface must be provided"
+            )
+        entry = hass.config_entries.async_get_entry(entry_id)
+        if entry and entry.domain == DOMAIN:
+            return entry
path: homeassistant/components/velbus/services.py
text: @user1:
We should also check that the config entry is loaded. Check Mealie for an example

diff_hunk: @@ -32,58 +35,76 @@ def setup_services(hass: HomeAssistant) -> None:
     """Register the velbus services."""
 
     def check_entry_id(interface: str) -> str:
+        """Check the config_entry for a specific interface."""
         for config_entry in hass.config_entries.async_entries(DOMAIN):
             if "port" in config_entry.data and config_entry.data["port"] == interface:
                 return config_entry.entry_id
         raise vol.Invalid(
             "The interface provided is not defined as a port in a Velbus integration"
         )
 
-    def get_config_entry(interface: str) -> VelbusConfigEntry | None:
-        for config_entry in hass.config_entries.async_entries(DOMAIN):
-            if "port" in config_entry.data and config_entry.data["port"] == interface:
-                return config_entry
-        return None
+    async def get_config_entry(call: ServiceCall) -> VelbusConfigEntry:
+        """Get the config entry for this service call."""
+        if CONF_CONFIG_ENTRY in call.data:
+            entry_id = call.data[CONF_CONFIG_ENTRY]
+        elif CONF_INTERFACE in call.data:
+            # Deprecated in 2025.2, to remove in 2025.9
+            async_create_issue(
+                hass,
+                DOMAIN,
+                "deprecated_interface_parameter",
+                breaks_in_ha_version="2025.9.0",
+                is_fixable=False,
+                severity=IssueSeverity.WARNING,
+                translation_key="deprecated_interface_parameter",
+            )
+            entry_id = call.data[CONF_INTERFACE]
+        else:
+            entry_id = None
+        if entry_id is None:
+            raise ServiceValidationError(
+                "Either config entry or interface must be provided"
+            )
path: homeassistant/components/velbus/services.py
text: @user1:
I think it would be better if you use `vol.Any` to make sure you at least get one

@author:
how should this work? not sure where to start

diff_hunk: @@ -32,58 +35,76 @@ def setup_services(hass: HomeAssistant) -> None:
     """Register the velbus services."""
 
     def check_entry_id(interface: str) -> str:
+        """Check the config_entry for a specific interface."""
         for config_entry in hass.config_entries.async_entries(DOMAIN):
             if "port" in config_entry.data and config_entry.data["port"] == interface:
                 return config_entry.entry_id
         raise vol.Invalid(
             "The interface provided is not defined as a port in a Velbus integration"
         )
 
-    def get_config_entry(interface: str) -> VelbusConfigEntry | None:
-        for config_entry in hass.config_entries.async_entries(DOMAIN):
-            if "port" in config_entry.data and config_entry.data["port"] == interface:
-                return config_entry
-        return None
+    async def get_config_entry(call: ServiceCall) -> VelbusConfigEntry:
+        """Get the config entry for this service call."""
+        if CONF_CONFIG_ENTRY in call.data:
+            entry_id = call.data[CONF_CONFIG_ENTRY]
+        elif CONF_INTERFACE in call.data:
+            # Deprecated in 2025.2, to remove in 2025.9
+            async_create_issue(
+                hass,
+                DOMAIN,
+                "deprecated_interface_parameter",
+                breaks_in_ha_version="2025.9.0",
+                is_fixable=False,
+                severity=IssueSeverity.WARNING,
+                translation_key="deprecated_interface_parameter",
+            )
+            entry_id = call.data[CONF_INTERFACE]
+        else:
+            entry_id = None
+        if entry_id is None:
path: homeassistant/components/velbus/services.py
text: @user1:
```suggestion
```