From 6e221dab9b29c16ea0dc9b1570252197921c116d Mon Sep 17 00:00:00 2001 From: John Vert Date: Mon, 10 Jul 2023 11:21:47 -0700 Subject: [PATCH] add support for windowShadeLevel capabaility (#72) * update aiohttp to 3.8.4 to fix build issue * add support for windowShadeLevel capability and setShadeLevel command. Fixes #67 * support shade_level attribute * add some set_window_shade_level tests * update black version to fix error due to click * update README to include set_window_shade_level() documentation * add shade_level property tests --- README.md | 7 +++ pysmartthings/capability.py | 2 + pysmartthings/device.py | 35 +++++++++++ requirements.txt | 2 +- test-requirements.txt | 2 +- .../device_command_post_set_shade_level.json | 10 ++++ tests/test_device.py | 59 +++++++++++++++++++ 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/json/device_command_post_set_shade_level.json diff --git a/README.md b/README.md index 641bf46..8a645e9 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,10 @@ Devices with the `switchLevel` capability have the following function that sets result = await device.set_level(75, 2) assert result == True ``` + +Devices with the `windowShadeLevel` capability have the following function that sets the target shade level. + +```pythonstub + result = await device.set_window_shade_level(50) + assert result == True +``` diff --git a/pysmartthings/capability.py b/pysmartthings/capability.py index 5e87f8b..23e019c 100644 --- a/pysmartthings/capability.py +++ b/pysmartthings/capability.py @@ -241,6 +241,7 @@ class Capability: washer_operating_state = "washerOperatingState" water_sensor = "waterSensor" window_shade = "windowShade" + window_shade_level = "windowShadeLevel" class Attribute: @@ -336,6 +337,7 @@ class Attribute: rssi = "rssi" saturation = "saturation" schedule = "schedule" + shade_level = "shadeLevel" smoke = "smoke" sound = "sound" st = "st" diff --git a/pysmartthings/device.py b/pysmartthings/device.py index a5d92f1..918f3fc 100644 --- a/pysmartthings/device.py +++ b/pysmartthings/device.py @@ -67,6 +67,7 @@ class Command: set_saturation = "setSaturation" set_thermostat_fan_mode = "setThermostatFanMode" set_thermostat_mode = "setThermostatMode" + set_shade_level = "setShadeLevel" unlock = "unlock" mute = "mute" unmute = "unmute" @@ -727,6 +728,18 @@ def media_title(self) -> bool: """Get the trackDescription attribute.""" return self._attributes["trackDescription"].value + @property + def shade_level(self) -> int: + """Get the shadeLevel attribute, scaled 0-100.""" + return int(self._attributes[Attribute.shade_level].value or 0) + + @shade_level.setter + def shade_level(self, value: int): + """Set the level of the attribute, scaled 0-100.""" + if not 0 <= value <= 100: + raise ValueError("value must be scaled between 0-100.") + self.update_attribute_value(Attribute.shade_level, value) + class DeviceStatus(DeviceStatusBase): """Define the device status.""" @@ -1390,6 +1403,28 @@ async def channel_down( component_id, Capability.tv_channel, Command.channel_down ) + async def set_window_shade_level( + self, + level: int, + set_status: bool = False, + *, + component_id: str = "main", + ) -> bool: + """Call the set shade level device command.""" + if not 0 <= level <= 100: + raise ValueError("level must be scaled between 0-100.") + + result = await self.command( + component_id, + Capability.window_shade_level, + Command.set_shade_level, + [level], + ) + if result and set_status: + self.status.shade_level = level + self.status.switch = level > 0 + return result + @property def status(self): """Get the status entity of the device.""" diff --git a/requirements.txt b/requirements.txt index 448801e..9a8a7f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -aiohttp==3.8.0 \ No newline at end of file +aiohttp==3.8.4 \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt index c9ebce7..8e881d8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -black==21.10b0 +black==22.3.0 coveralls==3.3.0 flake8==4.0.1 flake8-docstrings==1.6.0 diff --git a/tests/json/device_command_post_set_shade_level.json b/tests/json/device_command_post_set_shade_level.json new file mode 100644 index 0000000..5524eaf --- /dev/null +++ b/tests/json/device_command_post_set_shade_level.json @@ -0,0 +1,10 @@ +{ + "commands": [ + { + "component": "main", + "capability": "windowShadeLevel", + "command": "setShadeLevel", + "arguments": [75] + } + ] +} \ No newline at end of file diff --git a/tests/test_device.py b/tests/test_device.py index a1f9e19..db477fc 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1210,6 +1210,44 @@ async def test_channel_down(api): # Assert assert result + @staticmethod + @pytest.mark.asyncio + async def test_set_window_shade_level(api): + """Tests the set_window_shade_level method.""" + # Arrange + device = DeviceEntity(api, device_id=DEVICE_ID) + # Act + result = await device.set_window_shade_level(75) + # Assert + assert result + assert device.status.level == 0 + assert not device.status.switch + + @staticmethod + @pytest.mark.asyncio + async def test_set_window_shade_level_invalid(api): + """Tests the set_window_shade_level method invalid values.""" + # Arrange + device = DeviceEntity(api, device_id=DEVICE_ID) + # Assert level + levels = [-1, 101] + for level in levels: + with pytest.raises(ValueError): + await device.set_window_shade_level(level) + + @staticmethod + @pytest.mark.asyncio + async def test_set_window_shade_level_update(api): + """Tests the set_window_shade_level method.""" + # Arrange + device = DeviceEntity(api, device_id=DEVICE_ID) + # Act + result = await device.set_window_shade_level(75, True) + # Assert + assert result + assert device.status.shade_level == 75 + assert device.status.switch + class TestDeviceStatus: """Tests for the DeviceStatus class.""" @@ -1655,3 +1693,24 @@ def test_well_known_power_consumption_attributes(): assert status.power_consumption_energy_saved is None assert status.power_consumption_persisted_energy is None assert status.power_consumption_power_energy is None + + @staticmethod + def test_shade_level(): + """Tests the shade_level property.""" + # Arrange + status = DeviceStatus(None, device_id=DEVICE_ID) + # Act + status.shade_level = 50 + # Assert + assert status.shade_level == 50 + + @staticmethod + def test_shade_level_range(): + """Tests the shade_level property's range.""" + # Arrange + status = DeviceStatus(None, device_id=DEVICE_ID) + # Act/Assert + values = [-1, 101] + for value in values: + with pytest.raises(ValueError): + status.shade_level = value