Repository: webdjoe/pyvesync (language: python)

Issue:
Lighbulb not turning ON when setting brightness [regression]
With the code of the master branch, as of today,(not sure about last Pypi, didn't check) 
we can't turn on a lightbulb by setting it's brightness level, if the brightness level is the same as before, EVEN when the light bulb is currently OFF.

This is a regression from the latest refactoring, this used to work in the past.

Steps to reproduce:

```python
# setting up
from pyvesync import VeSync
mg = VeSync("###redacted_email###", "###redacted_secret###", "###region###", debug=False) 
mg.login()
mg.update()
mb0 = mg.bulbs[0]
mb0
mg.debug = True

#actual steps to reproduce issue
mb0.set_brightness(100)
mb0.turn_off()
mb0.set_brightness(100)
# here, the bulb will stay OFF when it shouldn't, no API call is made

```




Discussion / hints:
I found the offending lines of code. And there is similar broken logic for color_temperature and rgb changes as well for differnet bulbs. 
I'll do a PR to fix those this Sunday if I am able to find some time.

Fix patch:
diff --git a/src/pyvesync/helpers.py b/src/pyvesync/helpers.py
index d48fa3c..aed95d0 100644
--- a/src/pyvesync/helpers.py
+++ b/src/pyvesync/helpers.py
@@ -15,7 +15,9 @@
 
 API_BASE_URL = 'https://smartapi.vesync.com'
 API_RATE_LIMIT = 30
-API_TIMEOUT = 5
+# If device is out of reach, the cloud api sends a timeout response after 7 seconds,
+# using 8 here so there is time enough to catch that message
+API_TIMEOUT = 8
 USER_AGENT = ("VeSync/3.2.39 (com.etekcity.vesyncPlatform;"
               " build:5; iOS 15.5.0) Alamofire/5.2.1")
 
diff --git a/src/pyvesync/vesyncbulb.py b/src/pyvesync/vesyncbulb.py
index e6dfd15..9929eec 100644
--- a/src/pyvesync/vesyncbulb.py
+++ b/src/pyvesync/vesyncbulb.py
@@ -471,7 +471,7 @@ def set_status(self, brightness: Optional[NUMERIC_T] = None,
         if red is not None and green is not None and blue is not None:
             new_color = self._validate_rgb(red, green, blue)
             color_mode = 'color'
-            if new_color == self._color:
+            if self.device_status == 'on' and new_color == self._color:
                 logger.debug("New color is same as current color")
                 return True
         else:
@@ -479,6 +479,10 @@ def set_status(self, brightness: Optional[NUMERIC_T] = None,
             new_color = None
             if brightness is not None:
                 brightness_update = int(self._validate_brightness(brightness))
+                # Do nothing if brightness is passed and same as current
+                if self.device_status == 'on' and brightness_update == self._brightness:
+                    logger.debug('Brightness already set to %s', brightness)
+                    return True
                 color_mode = 'white'
             else:
                 logger.debug("Brightness and RGB values are not set")
@@ -627,7 +631,11 @@ def set_brightness(self, brightness: int) -> bool:
             logger.debug('%s is not dimmable', self.device_name)
             return False
         brightness_update = int(self._validate_brightness(brightness))
-
+        if self.device_status == 'on' and brightness_update == self._brightness:
+            logger.debug("Device already in requested state")
+            return True
+        if self.device_status == 'off':
+            self.toggle('on')
         body = helpers.req_body(self.manager, 'devicestatus')
         body['uuid'] = self.uuid
         body['status'] = 'on'
@@ -734,7 +742,8 @@ def toggle(self, status) -> bool:
     def set_brightness(self, brightness: int) -> bool:
         """Set brightness of tunable bulb."""
         brightness_update = int(self._validate_brightness(brightness))
-        if brightness_update == self._brightness:
+        if self.device_status == 'on' and brightness_update == self._brightness:
+            logger.debug("Device already in requested state")
             return True
         body = helpers.req_body(self.manager, 'bypass')
         body['cid'] = self.cid
@@ -765,7 +774,8 @@ def set_brightness(self, brightness: int) -> bool:
     def set_color_temp(self, color_temp: int) -> bool:
         """Set Color Temperature of Bulb in pct (1 - 100)."""
         color_temp_update = self._validate_color_temp(color_temp)
-        if color_temp_update == self._color_temp:
+        if self.device_status == 'on' and color_temp_update == self._color_temp:
+            logger.debug("Device already in requested state")
             return True
         body = helpers.req_body(self.manager, 'bypass')
         body['cid'] = self.cid
@@ -844,7 +854,8 @@ def _interpret_apicall_result(self, response) -> None:
                 sat = float(innerresult.get('saturation')/100)
                 val = float(innerresult.get('value'))
                 self._color = Color(hue=hue, saturation=sat, value=val)
-        elif response.get('code') == -11300030:
+        elif (response.get('code') == -11300030 or
+              response.get('code') == -11302030):
             logger.debug('%s device request timeout', self.device_name)
             self.connection_status = 'offline'
             self.device_status = 'off'
@@ -1001,7 +1012,7 @@ def set_hsv(self, hue: NUMERIC_T = None,
                 if val != "":
                     if val != current_dict[key]:
                         same_colors = False
-            if same_colors:
+            if self.device_status == 'on' and same_colors:
                 logger.debug("Device already in requested state")
                 return True
         for key, val in arg_dict.items():
@@ -1084,7 +1095,7 @@ def set_status(self,
             if all(locals().get(k) is None for k in force_list):
 
                 # Do nothing if brightness is passed and same as current
-                if brightness_update == self._brightness:
+                if self.device_status == 'on' and brightness_update == self._brightness:
                     logger.debug('Brightness already set to %s', brightness)
                     return True
                 request_dict['force'] = 0
diff --git a/tox.ini b/tox.ini
index d448b71..3ae1190 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,9 @@
 [tox]
-envlist = py39, pylint, lint, mypy
+envlist = py311, py310, py39, py38, pylint, lint, mypy
 skip_missing_interpreters = True
 ignore_basepython_conflict = True
 
 [testenv]
-basepython = {env:PYTHON3_PATH:python39}
 deps =
     pytest
     pyyaml


Test patch:
diff --git a/src/tests/test_x_vesync_bulbs.py b/src/tests/test_x_vesync_bulbs.py
index 83a38d5..a214909 100644
--- a/src/tests/test_x_vesync_bulbs.py
+++ b/src/tests/test_x_vesync_bulbs.py
@@ -77,6 +77,9 @@ def test_brightness(self):
         self.mock_api.return_value = ({'code': 0}, 200)
         bulb = VeSyncBulbESL100(DEV_LIST_DETAIL, self.manager)
         assert bulb.set_brightness(50)
+        assert bulb.turn_off()
+        assert bulb.set_brightness(50)
+        assert bulb.device_status == 'on'
 
     def test_invalid_brightness(self):
         self.mock_api.return_value = ({'code': 0}, 200)
@@ -137,6 +140,9 @@ def test_brightness(self):
         bulb = VeSyncBulbESL100CW(DEV_LIST_DETAIL_CW, self.manager)
         assert bulb.set_brightness(50)
         assert bulb.brightness == 50
+        assert bulb.turn_off()
+        assert bulb.set_brightness(50)
+        assert bulb.device_status == 'on'
 
     def test_invalid_brightness(self):
         self.mock_api.return_value = ({'code': 0}, 200)
@@ -197,6 +203,9 @@ def test_brightness(self):
         bulb = VeSyncBulbESL100MC(DEV_LIST_DETAIL_MC, self.manager)
         assert bulb.set_brightness(50)
         assert bulb.brightness == 50
+        assert bulb.turn_off()
+        assert bulb.set_brightness(50)
+        assert bulb.device_status == 'on'
 
     def test_invalid_brightness(self):
         self.mock_api.return_value = ({'code': 0}, 200)
@@ -258,6 +267,9 @@ def test_brightness(self):
         bulb = VeSyncBulbValcenoA19MC(DEV_LIST_DETAIL_VALCENO, self.manager)
         assert bulb.set_brightness(50)
         assert bulb.brightness == 50
+        assert bulb.turn_off()
+        assert bulb.set_brightness(50)
+        assert bulb.device_status == 'on'
 
     def test_invalid_brightness(self):
         """Test invalid brightness on Valceno."""