Nik Rolls 5 лет назад
Родитель
Сommit
25efc866a6

+ 6 - 0
.coveragerc

@@ -0,0 +1,6 @@
+[run]
+branch = True
+
+[report]
+skip_empty = True
+include = custom_components/*

+ 1 - 1
.devcontainer/devcontainer.json

@@ -15,7 +15,7 @@
     "files.exclude": {
       "config/custom_components/goldair_climate": true
     },
-    "python.pythonPath": "/usr/local/bin/python",
+    "python.pythonPath": "/usr/bin/python",
     "terminal.integrated.shell.linux": "/bin/bash"
   },
   "mounts": [

+ 5 - 6
.github/workflows/hacs-validate.yml

@@ -2,17 +2,16 @@ name: Validate with HACS
 
 on:
   push:
-  pull_request:
   schedule:
-    - cron: "0 0 * * *"
+    - cron: '0 0 * * *'
 
 jobs:
   validate:
-    runs-on: "ubuntu-latest"
+    runs-on: 'ubuntu-latest'
     steps:
-      - uses: "actions/checkout@v2"
+      - uses: 'actions/checkout@v2'
       - name: HACS validation
-        uses: "hacs/integration/action@master"
+        uses: 'hacs/integration/action@master'
         with:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          CATEGORY: "integration"
+          CATEGORY: 'integration'

+ 3 - 4
.github/workflows/hassfest-validate.yml

@@ -2,13 +2,12 @@ name: Validate with hassfest
 
 on:
   push:
-  pull_request:
   schedule:
-    - cron: "0 0 * * *"
+    - cron: '0 0 * * *'
 
 jobs:
   validate:
-    runs-on: "ubuntu-latest"
+    runs-on: 'ubuntu-latest'
     steps:
-      - uses: "actions/checkout@v2"
+      - uses: 'actions/checkout@v2'
       - uses: home-assistant/actions/hassfest@master

+ 4 - 2
.github/workflows/linting.yml

@@ -1,6 +1,6 @@
 name: Linting
 
-on: [push, pull_request]
+on: push
 
 jobs:
   lint:
@@ -14,7 +14,9 @@ jobs:
           python-version: 3.7
 
       - name: Install dependencies
-        run: pip install --pre -r requirements.txt
+        run: |
+          pip install -r requirements-first.txt
+          pip install --pre -r requirements-dev.txt
       - name: isort
         run: isort --recursive --diff
       - name: Black

+ 33 - 0
.github/workflows/tests.yml

@@ -0,0 +1,33 @@
+name: Python tests
+
+on: push
+
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [3.8]
+
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements-first.txt
+          pip install -r requirements-dev.txt
+      - name: Test with pytest
+        run: pytest --cov=. --cov-config=.coveragerc --cov-report xml:coverage.xml
+      - name: Track master branch
+        run: git fetch --no-tags https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY +refs/heads/master:refs/remotes/origin/master
+      - name: SonarCloud scan
+        uses: sonarsource/sonarcloud-github-action@master
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

+ 3 - 1
.gitignore

@@ -2,4 +2,6 @@
 /.vscode/
 __pycache__/
 /config/
-*.zip
+*.zip
+/.coverage
+/coverage.xml

+ 1 - 1
custom_components/goldair_climate/__init__.py

@@ -25,7 +25,7 @@ from .const import (
     CONF_TYPE,
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
-    CONF_TYPE_HEATER,
+    CONF_TYPE_GPPH_HEATER,
     SCAN_INTERVAL,
     CONF_TYPE_AUTO,
 )

+ 2 - 2
custom_components/goldair_climate/climate.py

@@ -9,7 +9,7 @@ from .const import (
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
-    CONF_TYPE_HEATER,
+    CONF_TYPE_GPPH_HEATER,
     CONF_CLIMATE,
     CONF_TYPE_AUTO,
 )
@@ -31,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         if discovery_info[CONF_TYPE] is None:
             raise ValueError(f"Unable to detect type for device {device.name}")
 
-    if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER:
+    if discovery_info[CONF_TYPE] == CONF_TYPE_GPPH_HEATER:
         data[CONF_CLIMATE] = GoldairHeater(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
         data[CONF_CLIMATE] = GoldairDehumidifier(device)

+ 2 - 2
custom_components/goldair_climate/configuration.py

@@ -5,7 +5,7 @@ from .const import (
     CONF_DEVICE_ID,
     CONF_LOCAL_KEY,
     CONF_TYPE,
-    CONF_TYPE_HEATER,
+    CONF_TYPE_GPPH_HEATER,
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
@@ -26,7 +26,7 @@ INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
         "type": vol.In(
             [
                 CONF_TYPE_AUTO,
-                CONF_TYPE_HEATER,
+                CONF_TYPE_GPPH_HEATER,
                 CONF_TYPE_DEHUMIDIFIER,
                 CONF_TYPE_FAN,
                 CONF_TYPE_GECO_HEATER,

+ 1 - 1
custom_components/goldair_climate/const.py

@@ -6,7 +6,7 @@ CONF_DEVICE_ID = "device_id"
 CONF_LOCAL_KEY = "local_key"
 CONF_TYPE = "type"
 CONF_TYPE_AUTO = "auto"
-CONF_TYPE_HEATER = "heater"
+CONF_TYPE_GPPH_HEATER = "heater"
 CONF_TYPE_DEHUMIDIFIER = "dehumidifier"
 CONF_TYPE_FAN = "fan"
 CONF_TYPE_GPCV_HEATER = "gpcv_heater"

+ 2 - 2
custom_components/goldair_climate/device.py

@@ -17,7 +17,7 @@ from .const import (
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
-    CONF_TYPE_HEATER,
+    CONF_TYPE_GPPH_HEATER,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -91,7 +91,7 @@ class GoldairTuyaDevice(object):
         if "8" in cached_state:
             return CONF_TYPE_FAN
         if "106" in cached_state:
-            return CONF_TYPE_HEATER
+            return CONF_TYPE_GPPH_HEATER
         if "7" in cached_state:
             return CONF_TYPE_GPCV_HEATER
         if "3" in cached_state:

+ 2 - 2
custom_components/goldair_climate/light.py

@@ -9,7 +9,7 @@ from .const import (
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
-    CONF_TYPE_HEATER,
+    CONF_TYPE_GPPH_HEATER,
     CONF_DISPLAY_LIGHT,
     CONF_TYPE_AUTO,
 )
@@ -29,7 +29,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         if discovery_info[CONF_TYPE] is None:
             raise ValueError(f"Unable to detect type for device {device.name}")
 
-    if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER:
+    if discovery_info[CONF_TYPE] == CONF_TYPE_GPPH_HEATER:
         data[CONF_DISPLAY_LIGHT] = GoldairHeaterLedDisplayLight(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
         data[CONF_DISPLAY_LIGHT] = GoldairDehumidifierLedDisplayLight(device)

+ 2 - 2
custom_components/goldair_climate/lock.py

@@ -9,7 +9,7 @@ from .const import (
     CONF_TYPE_FAN,
     CONF_TYPE_GECO_HEATER,
     CONF_TYPE_GPCV_HEATER,
-    CONF_TYPE_HEATER,
+    CONF_TYPE_GPPH_HEATER,
     CONF_CHILD_LOCK,
     CONF_TYPE_AUTO,
 )
@@ -34,7 +34,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         if discovery_info[CONF_TYPE] is None:
             raise ValueError(f"Unable to detect type for device {device.name}")
 
-    if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER:
+    if discovery_info[CONF_TYPE] == CONF_TYPE_GPPH_HEATER:
         data[CONF_CHILD_LOCK] = GoldairHeaterChildLock(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
         data[CONF_CHILD_LOCK] = GoldairDehumidifierChildLock(device)

+ 1 - 1
custom_components/goldair_climate/manifest.json

@@ -4,6 +4,6 @@
   "documentation": "https://github.com/nikrolls/homeassistant-goldair-climate",
   "dependencies": [],
   "codeowners": ["@nikrolls"],
-  "requirements": ["pytuya>=7.0.5"],
+  "requirements": ["pycrypto~=2.6.1", "pytuya~=7.0.5"],
   "config_flow": true
 }

+ 7 - 0
requirements-dev.txt

@@ -0,0 +1,7 @@
+homeassistant~=0.110.1
+pycrypto~=2.6.1
+pytuya~=7.0.5
+pytest~=5.4.2
+pytest-cov~=2.9.0
+black~=19.10b0
+isort~=4.3.21

+ 1 - 0
requirements-first.txt

@@ -0,0 +1 @@
+pycrypto~=2.6.1

+ 2 - 2
requirements.txt

@@ -1,2 +1,2 @@
-black==19.*
-isort==4.*
+pycrypto~=2.6.1
+pytuya~=7.0.5

+ 6 - 0
sonar-project.properties

@@ -0,0 +1,6 @@
+sonar.organization=nikrolls
+sonar.projectKey=nikrolls_homeassistant-goldair-climate
+
+sonar.sources=./custom_components/goldair_climate
+sonar.tests=./tests
+sonar.python.coverage.reportPaths=/github/workspace/coverage.xml

+ 0 - 0
tests/__init__.py


+ 44 - 0
tests/const.py

@@ -0,0 +1,44 @@
+GPPH_HEATER_PAYLOAD = {
+    "1": False,
+    "2": 25,
+    "3": 17,
+    "4": "C",
+    "6": True,
+    "12": 0,
+    "101": "5",
+    "102": 0,
+    "103": False,
+    "104": True,
+    "105": "auto",
+    "106": 20,
+}
+
+GPCV_HEATER_PAYLOAD = {
+    "1": True,
+    "2": True,
+    "3": 30,
+    "4": 25,
+    "5": 0,
+    "6": 0,
+    "7": "Low",
+}
+
+GECO_HEATER_PAYLOAD = {"1": True, "2": True, "3": 30, "4": 25, "5": 0, "6": 0}
+
+DEHUMIDIFIER_PAYLOAD = {
+    "1": False,
+    "2": "0",
+    "4": 30,
+    "5": False,
+    "6": "1",
+    "7": False,
+    "11": 0,
+    "12": "0",
+    "101": False,
+    "102": False,
+    "103": 20,
+    "104": 78,
+    "105": False,
+}
+
+FAN_PAYLOAD = {"1": False, "2": "12", "3": "normal", "8": True, "11": "0", "101": False}

+ 82 - 0
tests/test_device.py

@@ -0,0 +1,82 @@
+from unittest import IsolatedAsyncioTestCase
+from unittest.mock import patch
+
+from custom_components.goldair_climate.const import (
+    CONF_TYPE_DEHUMIDIFIER,
+    CONF_TYPE_FAN,
+    CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_GPCV_HEATER,
+    CONF_TYPE_GPPH_HEATER,
+)
+from custom_components.goldair_climate.device import GoldairTuyaDevice
+
+from .const import (
+    DEHUMIDIFIER_PAYLOAD,
+    FAN_PAYLOAD,
+    GECO_HEATER_PAYLOAD,
+    GPCV_HEATER_PAYLOAD,
+    GPPH_HEATER_PAYLOAD,
+)
+
+
+class TestDevice(IsolatedAsyncioTestCase):
+    def setUp(self):
+        patcher = patch("pytuya.Device")
+        self.addCleanup(patcher.stop)
+        self.mock_api = patcher.start()
+        self.subject = GoldairTuyaDevice(
+            "Some name", "some_dev_id", "some.ip.address", "some_local_key", None
+        )
+
+    def test_configures_pytuya_correctly(self):
+        self.mock_api.assert_called_once_with(
+            "some_dev_id", "some.ip.address", "some_local_key", "device"
+        )
+        self.assertIs(self.subject._api, self.mock_api())
+
+    def test_name(self):
+        """Returns the name given at instantiation."""
+        self.assertEqual("Some name", self.subject.name)
+
+    def test_unique_id(self):
+        """Returns the unique ID presented by the API class."""
+        self.assertIs(self.subject.unique_id, self.mock_api().id)
+
+    def test_device_info(self):
+        """Returns generic info plus the unique ID for categorisation."""
+        self.assertEqual(
+            self.subject.device_info,
+            {
+                "identifiers": {("goldair_climate", self.mock_api().id)},
+                "name": "Some name",
+                "manufacturer": "Goldair",
+            },
+        )
+
+    async def test_detects_geco_heater_payload(self):
+        self.subject._cached_state = GECO_HEATER_PAYLOAD
+        self.assertEqual(
+            await self.subject.async_inferred_type(), CONF_TYPE_GECO_HEATER
+        )
+
+    async def test_detects_gpcv_heater_payload(self):
+        self.subject._cached_state = GPCV_HEATER_PAYLOAD
+        self.assertEqual(
+            await self.subject.async_inferred_type(), CONF_TYPE_GPCV_HEATER
+        )
+
+    async def test_detects_gpph_heater_payload(self):
+        self.subject._cached_state = GPPH_HEATER_PAYLOAD
+        self.assertEqual(
+            await self.subject.async_inferred_type(), CONF_TYPE_GPPH_HEATER
+        )
+
+    async def test_detects_dehumidifier_payload(self):
+        self.subject._cached_state = DEHUMIDIFIER_PAYLOAD
+        self.assertEqual(
+            await self.subject.async_inferred_type(), CONF_TYPE_DEHUMIDIFIER
+        )
+
+    async def test_detects_fan_payload(self):
+        self.subject._cached_state = FAN_PAYLOAD
+        self.assertEqual(await self.subject.async_inferred_type(), CONF_TYPE_FAN)