diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json
index 44309ddf92415fbc16c5719326e510f2b5e49bf1..c27011d6bd10285b5cec2a8d1d52ea5c6ddfdcfa 100644
--- a/homeassistant/components/samsungtv/manifest.json
+++ b/homeassistant/components/samsungtv/manifest.json
@@ -6,7 +6,8 @@
     "getmac==0.8.2",
     "samsungctl[websocket]==0.7.1",
     "samsungtvws[async,encrypted]==2.5.0",
-    "wakeonlan==2.0.1"
+    "wakeonlan==2.0.1",
+    "async-upnp-client==0.27.0"
   ],
   "ssdp": [
     {
diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py
index edd929273b159990f8955e48fec32e2a04cbebe1..578408d98f3b1c9c73ccea5f2720d7aeab86898a 100644
--- a/homeassistant/components/samsungtv/media_player.py
+++ b/homeassistant/components/samsungtv/media_player.py
@@ -2,9 +2,15 @@
 from __future__ import annotations
 
 import asyncio
+from collections.abc import Coroutine
+import contextlib
 from datetime import datetime, timedelta
 from typing import Any
 
+from async_upnp_client.aiohttp import AiohttpSessionRequester
+from async_upnp_client.client import UpnpDevice, UpnpService
+from async_upnp_client.client_factory import UpnpFactory
+from async_upnp_client.exceptions import UpnpActionResponseError, UpnpConnectionError
 import voluptuous as vol
 from wakeonlan import send_magic_packet
 
@@ -24,12 +30,14 @@ from homeassistant.components.media_player.const import (
     SUPPORT_TURN_OFF,
     SUPPORT_TURN_ON,
     SUPPORT_VOLUME_MUTE,
+    SUPPORT_VOLUME_SET,
     SUPPORT_VOLUME_STEP,
 )
 from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
 from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import entity_component
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
 from homeassistant.helpers.entity import DeviceInfo
@@ -42,9 +50,11 @@ from .const import (
     CONF_MANUFACTURER,
     CONF_MODEL,
     CONF_ON_ACTION,
+    CONF_SSDP_RENDERING_CONTROL_LOCATION,
     DEFAULT_NAME,
     DOMAIN,
     LOGGER,
+    UPNP_SVC_RENDERINGCONTROL,
 )
 
 SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
@@ -104,6 +114,9 @@ class SamsungTVDevice(MediaPlayerEntity):
         self._config_entry = config_entry
         self._host: str | None = config_entry.data[CONF_HOST]
         self._mac: str | None = config_entry.data.get(CONF_MAC)
+        self._ssdp_rendering_control_location = config_entry.data.get(
+            CONF_SSDP_RENDERING_CONTROL_LOCATION
+        )
         self._on_script = on_script
         # Assume that the TV is in Play mode
         self._playing: bool = True
@@ -121,6 +134,8 @@ class SamsungTVDevice(MediaPlayerEntity):
         if self._on_script or self._mac:
             # Add turn-on if on_script or mac is available
             self._attr_supported_features |= SUPPORT_TURN_ON
+        if self._ssdp_rendering_control_location:
+            self._attr_supported_features |= SUPPORT_VOLUME_SET
 
         self._attr_device_info = DeviceInfo(
             name=self.name,
@@ -142,6 +157,8 @@ class SamsungTVDevice(MediaPlayerEntity):
         self._bridge.register_reauth_callback(self.access_denied)
         self._bridge.register_app_list_callback(self._app_list_callback)
 
+        self._upnp_device: UpnpDevice | None = None
+
     def _update_sources(self) -> None:
         self._attr_source_list = list(SOURCES)
         if app_list := self._app_list:
@@ -179,21 +196,77 @@ class SamsungTVDevice(MediaPlayerEntity):
                 STATE_ON if await self._bridge.async_is_on() else STATE_OFF
             )
 
-        if self._attr_state == STATE_ON and not self._app_list_event.is_set():
-            await self._bridge.async_request_app_list()
-            if self._app_list_event.is_set():
-                # The try+wait_for is a bit expensive so we should try not to
-                # enter it unless we have to (Python 3.11 will have zero cost try)
-                return
-            try:
-                await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY)
-            except asyncio.TimeoutError as err:
-                # No need to try again
-                self._app_list_event.set()
-                LOGGER.debug(
-                    "Failed to load app list from %s: %s", self._host, err.__repr__()
+        if self._attr_state != STATE_ON:
+            return
+
+        startup_tasks: list[Coroutine[Any, Any, None]] = []
+
+        if not self._app_list_event.is_set():
+            startup_tasks.append(self._async_startup_app_list())
+
+        if not self._upnp_device and self._ssdp_rendering_control_location:
+            startup_tasks.append(self._async_startup_upnp())
+
+        if startup_tasks:
+            await asyncio.gather(*startup_tasks)
+
+        if not (service := self._get_upnp_service()):
+            return
+
+        get_volume, get_mute = await asyncio.gather(
+            service.action("GetVolume").async_call(InstanceID=0, Channel="Master"),
+            service.action("GetMute").async_call(InstanceID=0, Channel="Master"),
+        )
+        LOGGER.debug("Upnp GetVolume on %s: %s", self._host, get_volume)
+        if (volume_level := get_volume.get("CurrentVolume")) is not None:
+            self._attr_volume_level = volume_level / 100
+        LOGGER.debug("Upnp GetMute on %s: %s", self._host, get_mute)
+        if (is_muted := get_mute.get("CurrentMute")) is not None:
+            self._attr_is_volume_muted = is_muted
+
+    async def _async_startup_app_list(self) -> None:
+        await self._bridge.async_request_app_list()
+        if self._app_list_event.is_set():
+            # The try+wait_for is a bit expensive so we should try not to
+            # enter it unless we have to (Python 3.11 will have zero cost try)
+            return
+        try:
+            await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY)
+        except asyncio.TimeoutError as err:
+            # No need to try again
+            self._app_list_event.set()
+            LOGGER.debug(
+                "Failed to load app list from %s: %s", self._host, err.__repr__()
+            )
+
+    async def _async_startup_upnp(self) -> None:
+        assert self._ssdp_rendering_control_location is not None
+        if self._upnp_device is None:
+            session = async_get_clientsession(self.hass)
+            upnp_requester = AiohttpSessionRequester(session)
+            upnp_factory = UpnpFactory(upnp_requester)
+            with contextlib.suppress(UpnpConnectionError):
+                self._upnp_device = await upnp_factory.async_create_device(
+                    self._ssdp_rendering_control_location
                 )
 
+    def _get_upnp_service(self, log: bool = False) -> UpnpService | None:
+        if self._upnp_device is None:
+            if log:
+                LOGGER.info("Upnp services are not available on %s", self._host)
+            return None
+
+        if service := self._upnp_device.services.get(UPNP_SVC_RENDERINGCONTROL):
+            return service
+
+        if log:
+            LOGGER.info(
+                "Upnp service %s is not available on %s",
+                UPNP_SVC_RENDERINGCONTROL,
+                self._host,
+            )
+        return None
+
     async def _async_launch_app(self, app_id: str) -> None:
         """Send launch_app to the tv."""
         if self._power_off_in_progress():
@@ -233,6 +306,19 @@ class SamsungTVDevice(MediaPlayerEntity):
         self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME
         await self._bridge.async_power_off()
 
+    async def async_set_volume_level(self, volume: float) -> None:
+        """Set volume level on the media player."""
+        if not (service := self._get_upnp_service(log=True)):
+            return
+        try:
+            await service.action("SetVolume").async_call(
+                InstanceID=0, Channel="Master", DesiredVolume=int(volume * 100)
+            )
+        except UpnpActionResponseError as err:
+            LOGGER.warning(
+                "Unable to set volume level on %s: %s", self._host, err.__repr__()
+            )
+
     async def async_volume_up(self) -> None:
         """Volume up the media player."""
         await self._async_send_keys(["KEY_VOLUP"])
diff --git a/requirements_all.txt b/requirements_all.txt
index 329671ed5d0c4ce162b8466b2b5b4373e70fe82c..0edc85003d143b40c5003edbd0b7b34fed5666b7 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -326,6 +326,7 @@ asterisk_mbox==0.5.0
 
 # homeassistant.components.dlna_dmr
 # homeassistant.components.dlna_dms
+# homeassistant.components.samsungtv
 # homeassistant.components.ssdp
 # homeassistant.components.upnp
 # homeassistant.components.yeelight
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 85ea66b009098577e2e591b7ea59dab03dc98a90..7001227f590e96776cf04b90cd87021ebeb947f4 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -259,6 +259,7 @@ arcam-fmj==0.12.0
 
 # homeassistant.components.dlna_dmr
 # homeassistant.components.dlna_dms
+# homeassistant.components.samsungtv
 # homeassistant.components.ssdp
 # homeassistant.components.upnp
 # homeassistant.components.yeelight
diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py
index 1358e8e0bb5302c762b716abfd214ca28c107b4f..045b8b6e6def6137aead58e6809489581b1d3ce5 100644
--- a/tests/components/samsungtv/__init__.py
+++ b/tests/components/samsungtv/__init__.py
@@ -1,6 +1,10 @@
 """Tests for the samsungtv component."""
 from __future__ import annotations
 
+from unittest.mock import Mock
+
+from async_upnp_client.client import UpnpAction, UpnpService
+
 from homeassistant.components.samsungtv.const import DOMAIN
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
@@ -21,3 +25,24 @@ async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> Config
     await hass.async_block_till_done()
 
     return entry
+
+
+def upnp_get_action_mock(device: Mock, service_type: str, action: str) -> Mock:
+    """Get or Add UpnpService/UpnpAction to UpnpDevice mock."""
+    upnp_service: Mock | None
+    if (upnp_service := device.services.get(service_type)) is None:
+        upnp_service = Mock(UpnpService)
+        upnp_service.actions = {}
+
+        def _get_action(action: str):
+            return upnp_service.actions.get(action)
+
+        upnp_service.action.side_effect = _get_action
+        device.services[service_type] = upnp_service
+
+    upnp_action: Mock | None
+    if (upnp_action := upnp_service.actions.get(action)) is None:
+        upnp_action = Mock(UpnpAction)
+        upnp_service.actions[action] = upnp_action
+
+    return upnp_action
diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py
index b602a3a9c52aaa17f87e4e88758fef413be16063..a3809726b5b8d056f319adfb984eadd9c20a04b5 100644
--- a/tests/components/samsungtv/conftest.py
+++ b/tests/components/samsungtv/conftest.py
@@ -6,6 +6,8 @@ from datetime import datetime
 from typing import Any
 from unittest.mock import AsyncMock, Mock, patch
 
+from async_upnp_client.client import UpnpDevice
+from async_upnp_client.exceptions import UpnpConnectionError
 import pytest
 from samsungctl import Remote
 from samsungtvws.async_remote import SamsungTVWSAsyncRemote
@@ -38,6 +40,28 @@ def app_list_delay_fixture() -> None:
         yield
 
 
+@pytest.fixture(name="upnp_factory", autouse=True)
+def upnp_factory_fixture() -> Mock:
+    """Patch UpnpFactory."""
+    with patch(
+        "homeassistant.components.samsungtv.media_player.UpnpFactory",
+        autospec=True,
+    ) as upnp_factory_class:
+        upnp_factory: Mock = upnp_factory_class.return_value
+        upnp_factory.async_create_device.side_effect = UpnpConnectionError
+        yield upnp_factory
+
+
+@pytest.fixture(name="upnp_device")
+async def upnp_device_fixture(upnp_factory: Mock) -> Mock:
+    """Patch async_upnp_client."""
+    upnp_device = Mock(UpnpDevice)
+    upnp_device.services = {}
+
+    with patch.object(upnp_factory, "async_create_device", side_effect=[upnp_device]):
+        yield upnp_device
+
+
 @pytest.fixture(name="remote")
 def remote_fixture() -> Mock:
     """Patch the samsungctl Remote."""
diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py
index f40777ccff53929c3ac8942341ca4e13a79c5141..546bede64670c297c562e642143298585c446ec0 100644
--- a/tests/components/samsungtv/test_media_player.py
+++ b/tests/components/samsungtv/test_media_player.py
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
 import logging
 from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch
 
+from async_upnp_client.exceptions import UpnpActionResponseError
 import pytest
 from samsungctl import exceptions
 from samsungtvws.async_remote import SamsungTVWSAsyncRemote
@@ -21,6 +22,7 @@ from homeassistant.components.media_player.const import (
     ATTR_INPUT_SOURCE,
     ATTR_MEDIA_CONTENT_ID,
     ATTR_MEDIA_CONTENT_TYPE,
+    ATTR_MEDIA_VOLUME_LEVEL,
     ATTR_MEDIA_VOLUME_MUTED,
     DOMAIN,
     MEDIA_TYPE_APP,
@@ -33,13 +35,17 @@ from homeassistant.components.media_player.const import (
 from homeassistant.components.samsungtv.const import (
     CONF_MODEL,
     CONF_ON_ACTION,
+    CONF_SSDP_RENDERING_CONTROL_LOCATION,
     DOMAIN as SAMSUNGTV_DOMAIN,
     ENCRYPTED_WEBSOCKET_PORT,
     METHOD_ENCRYPTED_WEBSOCKET,
     METHOD_WEBSOCKET,
     TIMEOUT_WEBSOCKET,
 )
-from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV
+from homeassistant.components.samsungtv.media_player import (
+    SUPPORT_SAMSUNGTV,
+    UPNP_SVC_RENDERINGCONTROL,
+)
 from homeassistant.const import (
     ATTR_DEVICE_CLASS,
     ATTR_ENTITY_ID,
@@ -62,6 +68,7 @@ from homeassistant.const import (
     SERVICE_TURN_ON,
     SERVICE_VOLUME_DOWN,
     SERVICE_VOLUME_MUTE,
+    SERVICE_VOLUME_SET,
     SERVICE_VOLUME_UP,
     STATE_OFF,
     STATE_ON,
@@ -73,7 +80,7 @@ from homeassistant.helpers.typing import ConfigType
 from homeassistant.setup import async_setup_component
 import homeassistant.util.dt as dt_util
 
-from . import setup_samsungtv_entry
+from . import setup_samsungtv_entry, upnp_get_action_mock
 from .const import (
     MOCK_ENTRYDATA_ENCRYPTED_WS,
     SAMPLE_DEVICE_INFO_FRAME,
@@ -119,6 +126,7 @@ MOCK_ENTRY_WS = {
     CONF_NAME: "fake",
     CONF_PORT: 8001,
     CONF_TOKEN: "123456789",
+    CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://any",
 }
 
 
@@ -1304,3 +1312,81 @@ async def test_websocket_unsupported_remote_control(
     assert entry.data[CONF_PORT] == ENCRYPTED_WEBSOCKET_PORT
     state = hass.states.get(ENTITY_ID)
     assert state.state == STATE_UNAVAILABLE
+
+
+@pytest.mark.usefixtures("remotews")
+async def test_volume_control_upnp(
+    hass: HomeAssistant, upnp_device: Mock, caplog: pytest.LogCaptureFixture
+) -> None:
+    """Test for Upnp volume control."""
+    upnp_get_volume = upnp_get_action_mock(
+        upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetVolume"
+    )
+    upnp_get_volume.async_call.return_value = {"CurrentVolume": 44}
+
+    upnp_get_mute = upnp_get_action_mock(
+        upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetMute"
+    )
+    upnp_get_mute.async_call.return_value = {"CurrentMute": False}
+
+    await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
+    upnp_get_volume.async_call.assert_called_once()
+    upnp_get_mute.async_call.assert_called_once()
+
+    # Upnp action succeeds
+    upnp_set_volume = upnp_get_action_mock(
+        upnp_device, UPNP_SVC_RENDERINGCONTROL, "SetVolume"
+    )
+    assert await hass.services.async_call(
+        DOMAIN,
+        SERVICE_VOLUME_SET,
+        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5},
+        True,
+    )
+    assert "Unable to set volume level on" not in caplog.text
+
+    # Upnp action failed
+    upnp_set_volume.async_call.side_effect = UpnpActionResponseError(
+        status=500, error_code=501, error_desc="Action Failed"
+    )
+    assert await hass.services.async_call(
+        DOMAIN,
+        SERVICE_VOLUME_SET,
+        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6},
+        True,
+    )
+    assert "Unable to set volume level on" in caplog.text
+
+
+@pytest.mark.usefixtures("remotews")
+async def test_upnp_not_available(
+    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
+) -> None:
+    """Test for volume control when Upnp is not available."""
+    await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
+
+    # Upnp action fails
+    assert await hass.services.async_call(
+        DOMAIN,
+        SERVICE_VOLUME_SET,
+        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6},
+        True,
+    )
+    assert "Upnp services are not available" in caplog.text
+
+
+@pytest.mark.usefixtures("remotews", "upnp_device")
+async def test_upnp_missing_service(
+    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
+) -> None:
+    """Test for volume control when Upnp is not available."""
+    await setup_samsungtv_entry(hass, MOCK_ENTRY_WS)
+
+    # Upnp action fails
+    assert await hass.services.async_call(
+        DOMAIN,
+        SERVICE_VOLUME_SET,
+        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6},
+        True,
+    )
+    assert f"Upnp service {UPNP_SVC_RENDERINGCONTROL} is not available" in caplog.text