Bläddra i källkod

Full coverage for GoldairTuyaDevice

Nik Rolls 5 år sedan
förälder
incheckning
da1633e403

+ 2 - 1
.devcontainer/devcontainer.json

@@ -9,7 +9,8 @@
     "ms-python.python",
     "visualstudioexptteam.vscodeintellicode",
     "redhat.vscode-yaml",
-    "esbenp.prettier-vscode"
+    "esbenp.prettier-vscode",
+    "ryanluker.vscode-coverage-gutters"
   ],
   "settings": {
     "files.exclude": {

+ 1 - 1
.vscode/tasks.json

@@ -34,7 +34,7 @@
     {
       "label": "Unit tests",
       "type": "shell",
-      "command": "pytest",
+      "command": "pytest --cov=. --cov-config=.coveragerc --cov-report xml:coverage.xml",
       "problemMatcher": []
     }
   ]

+ 1 - 10
custom_components/goldair_climate/device.py

@@ -41,7 +41,6 @@ class GoldairTuyaDevice(object):
         self._refresh_task = None
         self._rotate_api_protocol_version()
 
-        self._fixed_properties = {}
         self._reset_cached_state()
 
         self._TEMPERATURE_UNIT = TEMP_CELSIUS
@@ -99,13 +98,6 @@ class GoldairTuyaDevice(object):
 
         return None
 
-    def set_fixed_properties(self, fixed_properties):
-        self._fixed_properties = fixed_properties
-        set_fixed_properties = Timer(
-            10, lambda: self._set_properties(self._fixed_properties)
-        )
-        set_fixed_properties.start()
-
     async def async_refresh(self):
         last_updated = self._get_cached_state()["updated_at"]
         if self._refresh_task is None or time() - last_updated >= self._CACHE_TIMEOUT:
@@ -165,7 +157,6 @@ class GoldairTuyaDevice(object):
 
     def _add_properties_to_pending_updates(self, properties):
         now = time()
-        properties = {**properties, **self._fixed_properties}
 
         pending_updates = self._get_pending_updates()
         for key, value in properties.items():
@@ -249,4 +240,4 @@ class GoldairTuyaDevice(object):
     def get_key_for_value(obj, value, fallback=None):
         keys = list(obj.keys())
         values = list(obj.values())
-        return keys[values.index(value)] or fallback
+        return keys[values.index(value)] if value in values else fallback

+ 91 - 27
tests/test_device.py

@@ -1,9 +1,11 @@
+import threading
 from datetime import datetime, timedelta
-from time import time
+from time import sleep, time
 from unittest import IsolatedAsyncioTestCase
 from unittest.mock import AsyncMock, call, patch
 
 import pytest
+from homeassistant.const import TEMP_CELSIUS
 
 from custom_components.goldair_climate.const import (
     CONF_TYPE_DEHUMIDIFIER,
@@ -62,6 +64,17 @@ class TestDevice(IsolatedAsyncioTestCase):
             },
         )
 
+    def test_temperature_unit(self):
+        self.assertEqual(self.subject.temperature_unit, TEMP_CELSIUS)
+
+    async def test_refreshes_state_if_no_cached_state_exists(self):
+        self.subject._cached_state = {}
+        self.subject.async_refresh = AsyncMock()
+
+        await self.subject.async_inferred_type()
+
+        self.subject.async_refresh.assert_awaited()
+
     async def test_detects_geco_heater_payload(self):
         self.subject._cached_state = GECO_HEATER_PAYLOAD
         self.assertEqual(
@@ -90,6 +103,10 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.subject._cached_state = FAN_PAYLOAD
         self.assertEqual(await self.subject.async_inferred_type(), CONF_TYPE_FAN)
 
+    async def test_detection_returns_none_when_device_type_could_not_be_detected(self):
+        self.subject._cached_state = {"1": False}
+        self.assertEqual(await self.subject.async_inferred_type(), None)
+
     async def test_does_not_refresh_more_often_than_cache_timeout(self):
         refresh_task = AsyncMock()
         self.subject._cached_state = {"updated_at": time() - 19}
@@ -128,13 +145,13 @@ class TestDevice(IsolatedAsyncioTestCase):
         async_job.assert_awaited()
 
     def test_refresh_reloads_status_from_device(self):
-        self.subject._api.status.return_value = {"dps": {1: False}}
-        self.subject._cached_state = {1: True}
+        self.subject._api.status.return_value = {"dps": {"1": False}}
+        self.subject._cached_state = {"1": True}
 
         self.subject.refresh()
 
         self.subject._api.status.assert_called_once()
-        self.assertEqual(self.subject._cached_state[1], False)
+        self.assertEqual(self.subject._cached_state["1"], False)
         self.assertTrue(
             time() - 1 <= self.subject._cached_state["updated_at"] <= time()
         )
@@ -144,19 +161,19 @@ class TestDevice(IsolatedAsyncioTestCase):
             Exception("Error"),
             Exception("Error"),
             Exception("Error"),
-            {"dps": {1: False}},
+            {"dps": {"1": False}},
         ]
 
         self.subject.refresh()
 
         self.assertEqual(self.subject._api.status.call_count, 4)
-        self.assertEqual(self.subject._cached_state[1], False)
+        self.assertEqual(self.subject._cached_state["1"], False)
 
     def test_refresh_clears_cached_state_and_pending_updates_after_failing_four_times(
         self,
     ):
-        self.subject._cached_state = {1: True}
-        self.subject._pending_updates = {1: False}
+        self.subject._cached_state = {"1": True}
+        self.subject._pending_updates = {"1": False}
         self.subject._api.status.side_effect = [
             Exception("Error"),
             Exception("Error"),
@@ -187,8 +204,8 @@ class TestDevice(IsolatedAsyncioTestCase):
         )
 
     def test_reset_cached_state_clears_cached_state_and_pending_updates(self):
-        self.subject._cached_state = {1: True, "updated_at": time()}
-        self.subject._pending_updates = {1: False}
+        self.subject._cached_state = {"1": True, "updated_at": time()}
+        self.subject._pending_updates = {"1": False}
 
         self.subject._reset_cached_state()
 
@@ -196,41 +213,88 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.assertEqual(self.subject._pending_updates, {})
 
     def test_get_property_returns_value_from_cached_state(self):
-        self.subject._cached_state = {1: True}
-        self.assertEqual(self.subject.get_property(1), True)
+        self.subject._cached_state = {"1": True}
+        self.assertEqual(self.subject.get_property("1"), True)
 
     def test_get_property_returns_pending_update_value(self):
-        self.subject._pending_updates = {1: {"value": False, "updated_at": time() - 9}}
-        self.assertEqual(self.subject.get_property(1), False)
+        self.subject._pending_updates = {
+            "1": {"value": False, "updated_at": time() - 9}
+        }
+        self.assertEqual(self.subject.get_property("1"), False)
 
     def test_pending_update_value_overrides_cached_value(self):
-        self.subject._cached_state = {1: True}
-        self.subject._pending_updates = {1: {"value": False, "updated_at": time() - 9}}
+        self.subject._cached_state = {"1": True}
+        self.subject._pending_updates = {
+            "1": {"value": False, "updated_at": time() - 9}
+        }
 
-        self.assertEqual(self.subject.get_property(1), False)
+        self.assertEqual(self.subject.get_property("1"), False)
 
     def test_expired_pending_update_value_does_not_override_cached_value(self):
-        self.subject._cached_state = {1: True}
-        self.subject._pending_updates = {1: {"value": False, "updated_at": time() - 10}}
+        self.subject._cached_state = {"1": True}
+        self.subject._pending_updates = {
+            "1": {"value": False, "updated_at": time() - 10}
+        }
 
-        self.assertEqual(self.subject.get_property(1), True)
+        self.assertEqual(self.subject.get_property("1"), True)
 
     def test_get_property_returns_none_when_value_does_not_exist(self):
-        self.subject._cached_state = {1: True}
-        self.assertIs(self.subject.get_property(2), None)
+        self.subject._cached_state = {"1": True}
+        self.assertIs(self.subject.get_property("2"), None)
 
     async def test_async_set_property_schedules_job(self):
         async_job = AsyncMock()
         self.subject._hass.async_add_executor_job.return_value = awaitable = async_job()
 
-        await self.subject.async_set_property(1, False)
+        await self.subject.async_set_property("1", False)
 
         self.subject._hass.async_add_executor_job.assert_called_once_with(
-            self.subject.set_property, 1, False
+            self.subject.set_property, "1", False
         )
         async_job.assert_awaited()
 
     def test_set_property_immediately_stores_new_value_to_pending_updates(self):
-        self.subject.set_property(1, False)
-        self.subject._cached_state = {1: True}
-        self.assertEqual(self.subject.get_property(1), False)
+        self.subject.set_property("1", False)
+        self.subject._cached_state = {"1": True}
+        self.assertEqual(self.subject.get_property("1"), False)
+
+    def test_debounces_multiple_set_calls_into_one_api_call(self):
+        with patch("custom_components.goldair_climate.device.Timer") as mock:
+            self.subject.set_property("1", True)
+            mock.assert_called_once_with(1, self.subject._send_pending_updates)
+
+            debounce = self.subject._debounce
+            mock.reset_mock()
+
+            self.subject.set_property("2", False)
+            debounce.cancel.assert_called_once()
+            mock.assert_called_once_with(1, self.subject._send_pending_updates)
+
+            self.subject._api.generate_payload.return_value = "payload"
+            self.subject._send_pending_updates()
+            self.subject._api.generate_payload.assert_called_once_with(
+                "set", {"1": True, "2": False}
+            )
+            self.subject._api._send_receive.assert_called_once_with("payload")
+
+    def test_set_properties_takes_no_action_when_no_properties_are_provided(self):
+        with patch("custom_components.goldair_climate.device.Timer") as mock:
+            self.subject._set_properties({})
+            mock.assert_not_called()
+
+    def test_anticipate_property_value_updates_cached_state(self):
+        self.subject._cached_state = {"1": True}
+        self.subject.anticipate_property_value("1", False)
+        self.assertEqual(self.subject._cached_state["1"], False)
+
+    def test_get_key_for_value_returns_key_from_object_matching_value(self):
+        obj = {"key1": "value1", "key2": "value2"}
+
+        self.assertEqual(GoldairTuyaDevice.get_key_for_value(obj, "value1"), "key1")
+        self.assertEqual(GoldairTuyaDevice.get_key_for_value(obj, "value2"), "key2")
+
+    def test_get_key_for_value_returns_fallback_when_value_not_found(self):
+        obj = {"key1": "value1", "key2": "value2"}
+        self.assertEqual(
+            GoldairTuyaDevice.get_key_for_value(obj, "value3", fallback="fb"), "fb"
+        )