diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..11e0b3111a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +branch = True + +[report] +skip_empty = True +include = custom_components/* \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d27657b296..3e697115af 100644 --- a/.devcontainer/devcontainer.json +++ b/.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": [ diff --git a/.github/workflows/hacs-validate.yml b/.github/workflows/hacs-validate.yml index 9a79e20cb7..6b32425766 100644 --- a/.github/workflows/hacs-validate.yml +++ b/.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' diff --git a/.github/workflows/hassfest-validate.yml b/.github/workflows/hassfest-validate.yml index 18c7d19323..89beae448f 100644 --- a/.github/workflows/hassfest-validate.yml +++ b/.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 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index ccf8a51224..1c5e1486f2 100644 --- a/.github/workflows/linting.yml +++ b/.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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..4325930a7f --- /dev/null +++ b/.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 }} diff --git a/.gitignore b/.gitignore index 7e8f024da8..3b556cc925 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /.vscode/ __pycache__/ /config/ -*.zip \ No newline at end of file +*.zip +/.coverage +/coverage.xml \ No newline at end of file diff --git a/custom_components/goldair_climate/__init__.py b/custom_components/goldair_climate/__init__.py index 71fb2ecdfe..7874e91c00 100644 --- a/custom_components/goldair_climate/__init__.py +++ b/custom_components/goldair_climate/__init__.py @@ -25,7 +25,7 @@ CONF_TYPE, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, - CONF_TYPE_HEATER, + CONF_TYPE_GPPH_HEATER, SCAN_INTERVAL, CONF_TYPE_AUTO, ) diff --git a/custom_components/goldair_climate/climate.py b/custom_components/goldair_climate/climate.py index 5deb084a2b..9b1d94bbf5 100644 --- a/custom_components/goldair_climate/climate.py +++ b/custom_components/goldair_climate/climate.py @@ -9,7 +9,7 @@ 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) diff --git a/custom_components/goldair_climate/configuration.py b/custom_components/goldair_climate/configuration.py index 90f417bc22..90db863ae6 100644 --- a/custom_components/goldair_climate/configuration.py +++ b/custom_components/goldair_climate/configuration.py @@ -5,7 +5,7 @@ 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 @@ "type": vol.In( [ CONF_TYPE_AUTO, - CONF_TYPE_HEATER, + CONF_TYPE_GPPH_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_TYPE_GECO_HEATER, diff --git a/custom_components/goldair_climate/const.py b/custom_components/goldair_climate/const.py index 17737873fe..468eda2a1a 100644 --- a/custom_components/goldair_climate/const.py +++ b/custom_components/goldair_climate/const.py @@ -6,7 +6,7 @@ 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" diff --git a/custom_components/goldair_climate/device.py b/custom_components/goldair_climate/device.py index 106235bfe4..54dd8c11f9 100644 --- a/custom_components/goldair_climate/device.py +++ b/custom_components/goldair_climate/device.py @@ -17,7 +17,7 @@ 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 @@ async def async_inferred_type(self): 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: diff --git a/custom_components/goldair_climate/light.py b/custom_components/goldair_climate/light.py index ead8b174eb..1eaa7fe6ee 100644 --- a/custom_components/goldair_climate/light.py +++ b/custom_components/goldair_climate/light.py @@ -9,7 +9,7 @@ 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) diff --git a/custom_components/goldair_climate/lock.py b/custom_components/goldair_climate/lock.py index 78b78d9409..1e5f06193d 100644 --- a/custom_components/goldair_climate/lock.py +++ b/custom_components/goldair_climate/lock.py @@ -9,7 +9,7 @@ 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) diff --git a/custom_components/goldair_climate/manifest.json b/custom_components/goldair_climate/manifest.json index 76bc97c42b..fabeb83eed 100644 --- a/custom_components/goldair_climate/manifest.json +++ b/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 } diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000000..ad606ee8e0 --- /dev/null +++ b/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 diff --git a/requirements-first.txt b/requirements-first.txt new file mode 100644 index 0000000000..bb200354fc --- /dev/null +++ b/requirements-first.txt @@ -0,0 +1 @@ +pycrypto~=2.6.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2fe7490d29..224fbe7beb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -black==19.* -isort==4.* \ No newline at end of file +pycrypto~=2.6.1 +pytuya~=7.0.5 diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000..1692af00ed --- /dev/null +++ b/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 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/const.py b/tests/const.py new file mode 100644 index 0000000000..48251a769a --- /dev/null +++ b/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} diff --git a/tests/test_device.py b/tests/test_device.py new file mode 100644 index 0000000000..67fa85d83e --- /dev/null +++ b/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)