Repository: andrewsayre/pysmartthings

Issue:
Media devices support
Would it be possible to add support for media devices (TVs, soundbars, etc,)?
It seems that there already is some basic support for them (appropriate capabilities and attribute names), but there are no dedicated methods for executing commands nor attributes to retrieve current value.

https://smartthings.developer.samsung.com/docs/api-ref/capabilities.html#Media-Playback

Discussion:
andrewsayre: Make sure you're formatting the code with `black` :)
PiotrMachowski: That's much easier 😃 
PiotrMachowski: Is there any specific reason to keep so many classes (`Command`, `Device`, `DeviceStatusBase`, `DeviceStatus`, `DeviceEntity`) in one big file (`device.py`?
PiotrMachowski: @andrewsayre any progress here?
PiotrMachowski: @andrewsayre 
andrewsayre: Sorry for the delay, I will review this week and move this forward.
PiotrMachowski: No pressure :)

Patch:
diff --git a/pysmartthings/capability.py b/pysmartthings/capability.py
index 579e06c..155cc89 100644
--- a/pysmartthings/capability.py
+++ b/pysmartthings/capability.py
@@ -358,6 +358,7 @@ ATTRIBUTE_ON_VALUES = {
     Attribute.filter_status: "replace",
     Attribute.motion: "active",
     Attribute.mute: "muted",
+    Attribute.playback_shuffle: "enabled",
     Attribute.presence: "present",
     Attribute.sound: "detected",
     Attribute.switch: "on",
@@ -365,3 +366,18 @@ ATTRIBUTE_ON_VALUES = {
     Attribute.valve: "open",
     Attribute.water: "wet",
 }
+
+ATTRIBUTE_OFF_VALUES = {
+    Attribute.acceleration: "inactive",
+    Attribute.contact: "closed",
+    Attribute.filter_status: "normal",
+    Attribute.motion: "inactive",
+    Attribute.mute: "unmuted",
+    Attribute.playback_shuffle: "disabled",
+    Attribute.presence: "not present",
+    Attribute.sound: "not detected",
+    Attribute.switch: "off",
+    Attribute.tamper: "clear",
+    Attribute.valve: "closed",
+    Attribute.water: "dry",
+}
diff --git a/pysmartthings/device.py b/pysmartthings/device.py
index 878ff59..a15af6b 100644
--- a/pysmartthings/device.py
+++ b/pysmartthings/device.py
@@ -16,6 +16,7 @@ from typing import (
 
 from .api import Api
 from .capability import (
+    ATTRIBUTE_OFF_VALUES,
     ATTRIBUTE_ON_VALUES,
     Attribute,
     Capability,
@@ -52,6 +53,11 @@ def hex_to_hs(color_hex: str) -> (int, int):
     return round(hsv[0] * 100, 3), round(hsv[1] * 100, 3)
 
 
+def bool_to_value(attribute: str, value: bool) -> str:
+    """Convert bool value to ON/OFF value of given attribute."""
+    return ATTRIBUTE_ON_VALUES[attribute] if value else ATTRIBUTE_OFF_VALUES[attribute]
+
+
 class Command:
     """Define common commands."""
 
@@ -78,6 +84,22 @@ class Command:
     set_thermostat_fan_mode = "setThermostatFanMode"
     set_thermostat_mode = "setThermostatMode"
     unlock = "unlock"
+    mute = "mute"
+    unmute = "unmute"
+    set_volume = "setVolume"
+    volume_up = "volumeUp"
+    volume_down = "volumeDown"
+    play = "play"
+    pause = "pause"
+    stop = "stop"
+    fast_forward = "fastForward"
+    rewind = "rewind"
+    set_input_source = "setInputSource"
+    set_playback_shuffle = "setPlaybackShuffle"
+    set_playback_repeat_mode = "setPlaybackRepeatMode"
+    set_tv_channel = "setTvChannel"
+    channel_up = "channelUp"
+    channel_down = "channelDown"
 
 
 class Device:
@@ -308,7 +330,7 @@ class DeviceStatusBase:
     @switch.setter
     def switch(self, value: bool):
         """Set the value of the switch attribute."""
-        status_value = ATTRIBUTE_ON_VALUES[Attribute.switch] if value else "off"
+        status_value = bool_to_value(Attribute.switch, value)
         self.update_attribute_value(Attribute.switch, status_value)
 
     @property
@@ -588,6 +610,97 @@ class DeviceStatusBase:
         """Get the three axis attribute."""
         return self._attributes[Attribute.three_axis].value
 
+    @property
+    def mute(self) -> bool:
+        """Get the mute attribute."""
+        return self.is_on(Attribute.mute)
+
+    @mute.setter
+    def mute(self, value: bool):
+        """Set the mute attribute."""
+        status_value = bool_to_value(Attribute.mute, value)
+        self.update_attribute_value(Attribute.mute, status_value)
+
+    @property
+    def volume(self) -> int:
+        """Get the volume attribute."""
+        return self._attributes[Attribute.volume].value
+
+    @volume.setter
+    def volume(self, value: float):
+        """Set the volume attribute, scaled 0-100."""
+        if not 0 <= value <= 100:
+            raise ValueError("value must be scaled between 0-100.")
+        self.update_attribute_value(Attribute.volume, value)
+
+    @property
+    def playback_status(self) -> str:
+        """Get the playbackStatus attribute."""
+        return self._attributes[Attribute.playback_status].value
+
+    @playback_status.setter
+    def playback_status(self, value: str):
+        """Set the playbackStatus attribute."""
+        self.update_attribute_value(Attribute.playback_status, value)
+
+    @property
+    def input_source(self) -> str:
+        """Get the inputSource attribute."""
+        return self._attributes[Attribute.input_source].value
+
+    @input_source.setter
+    def input_source(self, value: str):
+        """Set the volume attribute."""
+        if value not in self.supported_input_sources:
+            raise ValueError("value must be supported.")
+        self.update_attribute_value(Attribute.input_source, value)
+
+    @property
+    def supported_input_sources(self) -> Sequence[str]:
+        """Get the supportedInputSources attribute."""
+        value = self._attributes[Attribute.supported_input_sources].value
+        if "value" in value:
+            return value["value"]
+        return value
+
+    @property
+    def playback_shuffle(self) -> bool:
+        """Get the playbackShuffle attribute."""
+        return self.is_on(Attribute.playback_shuffle)
+
+    @playback_shuffle.setter
+    def playback_shuffle(self, value: bool):
+        """Set the playbackShuffle attribute."""
+        status_value = bool_to_value(Attribute.playback_shuffle, value)
+        self.update_attribute_value(Attribute.playback_shuffle, status_value)
+
+    @property
+    def playback_repeat_mode(self) -> str:
+        """Get the playbackRepeatMode attribute."""
+        return self._attributes[Attribute.playback_repeat_mode].value
+
+    @playback_repeat_mode.setter
+    def playback_repeat_mode(self, value: str):
+        """Set the playbackRepeatMode attribute."""
+        if value not in ["all", "off", "one"]:
+            raise ValueError("value must be one of: all, off, one")
+        self.update_attribute_value(Attribute.playback_repeat_mode, value)
+
+    @property
+    def tv_channel(self) -> str:
+        """Get the tvChannel attribute."""
+        return self._attributes[Attribute.tv_channel].value
+
+    @tv_channel.setter
+    def tv_channel(self, value: str):
+        """Set the tvChannel attribute."""
+        self.update_attribute_value(Attribute.tv_channel, value)
+
+    @property
+    def media_title(self) -> bool:
+        """Get the trackDescription attribute."""
+        return self._attributes["trackDescription"].value
+
 
 class DeviceStatus(DeviceStatusBase):
     """Define the device status."""
@@ -1061,6 +1174,182 @@ class DeviceEntity(Entity, Device):
             self.status.update_attribute_value(Attribute.air_flow_direction, direction)
         return result
 
+    async def mute(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the mute command."""
+        result = await self.command(component_id, Capability.audio_mute, Command.mute)
+        if result and set_status:
+            self.status.mute = True
+        return result
+
+    async def unmute(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the unmute command."""
+        result = await self.command(component_id, Capability.audio_mute, Command.unmute)
+        if result and set_status:
+            self.status.mute = False
+        return result
+
+    async def set_volume(
+        self, volume: int, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the setVolume command."""
+        result = await self.command(
+            component_id, Capability.audio_volume, Command.set_volume, [volume]
+        )
+        if result and set_status:
+            self.status.volume = volume
+        return result
+
+    async def volume_up(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the volumeUp command."""
+        result = await self.command(
+            component_id, Capability.audio_volume, Command.volume_up
+        )
+        if result and set_status:
+            self.status.volume = min(self.status.volume + 1, 100)
+        return result
+
+    async def volume_down(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the volumeDown command."""
+        result = await self.command(
+            component_id, Capability.audio_volume, Command.volume_down
+        )
+        if result and set_status:
+            self.status.volume = max(self.status.volume - 1, 0)
+        return result
+
+    async def play(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the play command."""
+        result = await self.command(
+            component_id, Capability.media_playback, Command.play
+        )
+        if result and set_status:
+            self.status.playback_status = "play"
+        return result
+
+    async def pause(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the pause command."""
+        result = await self.command(
+            component_id, Capability.media_playback, Command.pause
+        )
+        if result and set_status:
+            self.status.playback_status = "pause"
+        return result
+
+    async def stop(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the stop command."""
+        result = await self.command(
+            component_id, Capability.media_playback, Command.stop
+        )
+        if result and set_status:
+            self.status.playback_status = "stop"
+        return result
+
+    async def fast_forward(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the fastForward command."""
+        result = await self.command(
+            component_id, Capability.media_playback, Command.fast_forward
+        )
+        if result and set_status:
+            self.status.playback_status = "fast forward"
+        return result
+
+    async def rewind(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the rewind command."""
+        result = await self.command(
+            component_id, Capability.media_playback, Command.rewind
+        )
+        if result and set_status:
+            self.status.playback_status = "rewind"
+        return result
+
+    async def set_input_source(
+        self, source: str, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the setInputSource command."""
+        result = await self.command(
+            component_id,
+            Capability.media_input_source,
+            Command.set_input_source,
+            [source],
+        )
+        if result and set_status:
+            self.status.input_source = source
+        return result
+
+    async def set_playback_shuffle(
+        self, shuffle: bool, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the setPlaybackShuffle command."""
+        shuffle_value = bool_to_value(Attribute.playback_shuffle, shuffle)
+        result = await self.command(
+            component_id,
+            Capability.media_playback_shuffle,
+            Command.set_playback_shuffle,
+            [shuffle_value],
+        )
+        if result and set_status:
+            self.status.playback_shuffle = shuffle
+        return result
+
+    async def set_repeat(
+        self, repeat: str, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the setPlaybackRepeatMode command."""
+        result = await self.command(
+            component_id,
+            Capability.media_playback_repeat,
+            Command.set_playback_repeat_mode,
+            [repeat],
+        )
+        if result and set_status:
+            self.status.playback_repeat_mode = repeat
+        return result
+
+    async def set_tv_channel(
+        self, channel: str, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the setTvChannel command."""
+        result = await self.command(
+            component_id, Capability.tv_channel, Command.set_tv_channel, [channel]
+        )
+        if result and set_status:
+            self.status.tv_channel = channel
+        return result
+
+    async def channel_up(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the channelUp command."""
+        return await self.command(
+            component_id, Capability.tv_channel, Command.channel_up
+        )
+
+    async def channel_down(
+        self, set_status: bool = False, *, component_id: str = "main"
+    ) -> bool:
+        """Call the channelDown command."""
+        return await self.command(
+            component_id, Capability.tv_channel, Command.channel_down
+        )
+
     @property
     def status(self):
         """Get the status entity of the device."""


Test patch:
diff --git a/tests/json/device_command_post_channel_down.json b/tests/json/device_command_post_channel_down.json
new file mode 100644
index 0000000..ace9c63
--- /dev/null
+++ b/tests/json/device_command_post_channel_down.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "tvChannel",
+      "command": "channelDown"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_channel_up.json b/tests/json/device_command_post_channel_up.json
new file mode 100644
index 0000000..6c6c83b
--- /dev/null
+++ b/tests/json/device_command_post_channel_up.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "tvChannel",
+      "command": "channelUp"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_fast_forward.json b/tests/json/device_command_post_fast_forward.json
new file mode 100644
index 0000000..8ab327f
--- /dev/null
+++ b/tests/json/device_command_post_fast_forward.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaPlayback",
+      "command": "fastForward"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_mute.json b/tests/json/device_command_post_mute.json
new file mode 100644
index 0000000..b216edc
--- /dev/null
+++ b/tests/json/device_command_post_mute.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "audioMute",
+      "command": "mute"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_pause.json b/tests/json/device_command_post_pause.json
new file mode 100644
index 0000000..dbce642
--- /dev/null
+++ b/tests/json/device_command_post_pause.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaPlayback",
+      "command": "pause"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_play.json b/tests/json/device_command_post_play.json
new file mode 100644
index 0000000..1136968
--- /dev/null
+++ b/tests/json/device_command_post_play.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaPlayback",
+      "command": "play"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_rewind.json b/tests/json/device_command_post_rewind.json
new file mode 100644
index 0000000..c0773da
--- /dev/null
+++ b/tests/json/device_command_post_rewind.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaPlayback",
+      "command": "rewind"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_set_input_source.json b/tests/json/device_command_post_set_input_source.json
new file mode 100644
index 0000000..16d921c
--- /dev/null
+++ b/tests/json/device_command_post_set_input_source.json
@@ -0,0 +1,10 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaInputSource",
+      "command": "setInputSource",
+      "arguments": ["Source"]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_set_playback_shuffle.json b/tests/json/device_command_post_set_playback_shuffle.json
new file mode 100644
index 0000000..4864952
--- /dev/null
+++ b/tests/json/device_command_post_set_playback_shuffle.json
@@ -0,0 +1,10 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaPlaybackShuffle",
+      "command": "setPlaybackShuffle",
+      "arguments": ["enabled"]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_set_repeat.json b/tests/json/device_command_post_set_repeat.json
new file mode 100644
index 0000000..5481a31
--- /dev/null
+++ b/tests/json/device_command_post_set_repeat.json
@@ -0,0 +1,10 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaPlaybackRepeat",
+      "command": "setPlaybackRepeatMode",
+      "arguments": ["all"]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_set_tv_channel.json b/tests/json/device_command_post_set_tv_channel.json
new file mode 100644
index 0000000..9e13995
--- /dev/null
+++ b/tests/json/device_command_post_set_tv_channel.json
@@ -0,0 +1,10 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "tvChannel",
+      "command": "setTvChannel",
+      "arguments": ["Channel"]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_set_volume.json b/tests/json/device_command_post_set_volume.json
new file mode 100644
index 0000000..3fd39cb
--- /dev/null
+++ b/tests/json/device_command_post_set_volume.json
@@ -0,0 +1,10 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "audioVolume",
+      "command": "setVolume",
+      "arguments": [10]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_stop.json b/tests/json/device_command_post_stop.json
new file mode 100644
index 0000000..87c328c
--- /dev/null
+++ b/tests/json/device_command_post_stop.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "mediaPlayback",
+      "command": "stop"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_unmute.json b/tests/json/device_command_post_unmute.json
new file mode 100644
index 0000000..a70ff66
--- /dev/null
+++ b/tests/json/device_command_post_unmute.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "audioMute",
+      "command": "unmute"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_volume_down.json b/tests/json/device_command_post_volume_down.json
new file mode 100644
index 0000000..ca77fb3
--- /dev/null
+++ b/tests/json/device_command_post_volume_down.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "audioVolume",
+      "command": "volumeDown"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/json/device_command_post_volume_up.json b/tests/json/device_command_post_volume_up.json
new file mode 100644
index 0000000..1fd4912
--- /dev/null
+++ b/tests/json/device_command_post_volume_up.json
@@ -0,0 +1,9 @@
+{
+  "commands": [
+    {
+      "component": "main",
+      "capability": "audioVolume",
+      "command": "volumeUp"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/test_device.py b/tests/test_device.py
index ae4a318..e008f7e 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -812,6 +812,398 @@ class TestDeviceEntity:
         assert await device.set_air_flow_direction("fixed", set_status=True)
         assert device.status.air_flow_direction == "fixed"
 
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_mute(api):
+        """Test the mute method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.mute = False
+        # Act
+        result = await device.mute()
+        # Assert
+        assert result
+        assert not device.status.mute
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_mute_update(api):
+        """Test the mute method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.mute = False
+        # Act
+        result = await device.mute(set_status=True)
+        # Assert
+        assert result
+        assert device.status.mute
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_unmute(api):
+        """Test the unmute method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.mute = True
+        # Act
+        result = await device.unmute()
+        # Assert
+        assert result
+        assert device.status.mute
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_unmute_update(api):
+        """Test the unmute method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.mute = True
+        # Act
+        result = await device.unmute(set_status=True)
+        # Assert
+        assert result
+        assert not device.status.mute
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_volume(api):
+        """Test the set_volume method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.volume = 9
+        # Act
+        result = await device.set_volume(volume=10)
+        # Assert
+        assert result
+        assert device.status.volume == 9
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_volume_update(api):
+        """Test the set_volume method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.volume = 9
+        # Act
+        result = await device.set_volume(volume=10, set_status=True)
+        # Assert
+        assert result
+        assert device.status.volume == 10
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_volume_up(api):
+        """Test the volume_up method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.volume = 9
+        # Act
+        result = await device.volume_up()
+        # Assert
+        assert result
+        assert device.status.volume == 9
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_volume_up_update(api):
+        """Test the volume_up method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.volume = 9
+        # Act
+        result = await device.volume_up(set_status=True)
+        # Assert
+        assert result
+        assert device.status.volume == 10
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_volume_down(api):
+        """Test the volume_down method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.volume = 11
+        # Act
+        result = await device.volume_down()
+        # Assert
+        assert result
+        assert device.status.volume == 11
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_volume_down_update(api):
+        """Test the volume_down method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.volume = 11
+        # Act
+        result = await device.volume_down(set_status=True)
+        # Assert
+        assert result
+        assert device.status.volume == 10
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_play(api):
+        """Test the play method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.play()
+        # Assert
+        assert result
+        assert device.status.playback_status is None
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_play_update(api):
+        """Test the play method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.play(set_status=True)
+        # Assert
+        assert result
+        assert device.status.playback_status == "play"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_pause(api):
+        """Test the pause method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.pause()
+        # Assert
+        assert result
+        assert device.status.playback_status is None
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_pause_update(api):
+        """Test the pause method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.pause(set_status=True)
+        # Assert
+        assert result
+        assert device.status.playback_status == "pause"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_stop(api):
+        """Test the stop method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.stop()
+        # Assert
+        assert result
+        assert device.status.playback_status is None
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_stop_update(api):
+        """Test the stop method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.stop(set_status=True)
+        # Assert
+        assert result
+        assert device.status.playback_status == "stop"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_fast_forward(api):
+        """Test the fast_forward method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.fast_forward()
+        # Assert
+        assert result
+        assert device.status.playback_status is None
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_fast_forward_update(api):
+        """Test the fast_forward method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.fast_forward(set_status=True)
+        # Assert
+        assert result
+        assert device.status.playback_status == "fast forward"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_rewind(api):
+        """Test the rewind method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.rewind()
+        # Assert
+        assert result
+        assert device.status.playback_status is None
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_rewind_update(api):
+        """Test the rewind method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_status = None
+        # Act
+        result = await device.rewind(set_status=True)
+        # Assert
+        assert result
+        assert device.status.playback_status == "rewind"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_input_source(api):
+        """Test the set_input_source method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.update_attribute_value(
+            Attribute.supported_input_sources, ["Source", "OtherSource"]
+        )
+        device.status.input_source = "OtherSource"
+        # Act
+        result = await device.set_input_source("Source")
+        # Assert
+        assert result
+        assert device.status.input_source == "OtherSource"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_input_source_update(api):
+        """Test the set_input_source method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.update_attribute_value(
+            Attribute.supported_input_sources, ["Source", "OtherSource"]
+        )
+        device.status.input_source = "OtherSource"
+        # Act
+        result = await device.set_input_source("Source", set_status=True)
+        # Assert
+        assert result
+        assert device.status.input_source == "Source"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_playback_shuffle(api):
+        """Test the set_playback_shuffle method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_shuffle = False
+        # Act
+        result = await device.set_playback_shuffle(shuffle=True)
+        # Assert
+        assert result
+        assert not device.status.playback_shuffle
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_playback_shuffle_update(api):
+        """Test the set_playback_shuffle method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_shuffle = False
+        # Act
+        result = await device.set_playback_shuffle(True, set_status=True)
+        # Assert
+        assert result
+        assert device.status.playback_shuffle
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_repeat(api):
+        """Test the set_repeat method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_repeat_mode = "off"
+        # Act
+        result = await device.set_repeat("all")
+        # Assert
+        assert result
+        assert device.status.playback_repeat_mode == "off"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_repeat_update(api):
+        """Test the set_repeat method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.playback_repeat_mode = "off"
+        # Act
+        result = await device.set_repeat("all", set_status=True)
+        # Assert
+        assert result
+        assert device.status.playback_repeat_mode == "all"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_tv_channel(api):
+        """Test the tv_channel method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.tv_channel = "OtherChannel"
+        # Act
+        result = await device.set_tv_channel("Channel")
+        # Assert
+        assert result
+        assert device.status.tv_channel == "OtherChannel"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_set_tv_channel_update(api):
+        """Test the tv_channel method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        device.status.tv_channel = "OtherChannel"
+        # Act
+        result = await device.set_tv_channel("Channel", set_status=True)
+        # Assert
+        assert result
+        assert device.status.tv_channel == "Channel"
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_channel_up(api):
+        """Test the channel_up method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        # Act
+        result = await device.channel_up()
+        # Assert
+        assert result
+
+    @staticmethod
+    @pytest.mark.asyncio
+    async def test_channel_down(api):
+        """Test the channel_down method."""
+        # Arrange
+        device = DeviceEntity(api, device_id=DEVICE_ID)
+        # Act
+        result = await device.channel_down()
+        # Assert
+        assert result
+
 
 class TestDeviceStatus:
     """Tests for the DeviceStatus class."""
@@ -1042,6 +1434,36 @@ class TestDeviceStatus:
             with pytest.raises(ValueError):
                 status.color = value
 
+    @staticmethod
+    def test_volume_range(api):
+        """Test the volume property's range."""
+        # Arrange
+        status = DeviceStatus(api, device_id=DEVICE_ID)
+        # Act/Assert
+        values = [-1, 101]
+        for value in values:
+            with pytest.raises(ValueError):
+                status.volume = value
+
+    @staticmethod
+    def test_input_source(api):
+        """Test the volume property's range."""
+        # Arrange
+        status = DeviceStatus(api, device_id=DEVICE_ID)
+        status.update_attribute_value(Attribute.supported_input_sources, "Source")
+        # Act/Assert
+        with pytest.raises(ValueError):
+            status.input_source = "INVALID"
+
+    @staticmethod
+    def test_playback_repeat_mode(api):
+        """Test the volume property's range."""
+        # Arrange
+        status = DeviceStatus(api, device_id=DEVICE_ID)
+        # Act/Assert
+        with pytest.raises(ValueError):
+            status.playback_repeat_mode = "INVALID"
+
     @staticmethod
     def test_is_on():
         """Tests the is_on method."""