From a23e5d5214fde1e1d4d448d5c27e73983c834af0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 10 Apr 2023 19:37:16 -0400 Subject: [PATCH 1/8] Use shared CI and add ruff (#2323) * use shared ci and add ruff * rename file * tox * add matchers * fix docstring * align precommit * code folder * bump cache * setup env correctly * use with * try this * resort to duplication * copy pasta * shared coverage * cleanup * minimum coverage percentage * cleanup * cleanup --- .github/workflows/ci.yml | 384 +----------------- .github/workflows/matchers/ruff.json | 30 ++ .pre-commit-config.yaml | 28 +- ...ents_test_all.txt => requirements_test.txt | 1 + ruff.toml | 71 ++++ script/setup | 2 +- tests/test_inovelli_blue.py | 1 - tests/test_tuya.py | 8 - tests/test_tuya_dimmer.py | 1 - tests/test_xiaomi.py | 5 +- tox.ini | 4 +- zhaquirks/__init__.py | 22 +- zhaquirks/eurotronic/__init__.py | 2 - zhaquirks/kof/kof_mr101z.py | 2 +- zhaquirks/samjin/__init__.py | 2 +- zhaquirks/smartwings/wm25lz.py | 2 +- zhaquirks/terncy/__init__.py | 2 +- zhaquirks/tuya/__init__.py | 4 +- zhaquirks/tuya/mcu/__init__.py | 3 +- zhaquirks/tuya/ts0601_dimmer.py | 2 - zhaquirks/tuya/ts0601_trv.py | 4 - zhaquirks/tuya/ts0601_valve.py | 3 +- zhaquirks/xbee/__init__.py | 4 +- zhaquirks/xbee/types.py | 3 +- zhaquirks/xiaomi/aqara/cube.py | 1 - zhaquirks/xiaomi/aqara/cube_aqgl01.py | 1 - zhaquirks/xiaomi/aqara/motion_ac02.py | 2 +- zhaquirks/xiaomi/aqara/opple_remote.py | 2 +- zhaquirks/xiaomi/aqara/opple_switch.py | 2 +- zhaquirks/xiaomi/aqara/plug_eu.py | 3 +- zhaquirks/xiaomi/aqara/remote_b286acn01.py | 2 +- zhaquirks/xiaomi/aqara/roller_curtain_e1.py | 1 - 32 files changed, 158 insertions(+), 446 deletions(-) create mode 100644 .github/workflows/matchers/ruff.json rename requirements_test_all.txt => requirements_test.txt (92%) create mode 100644 ruff.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 087698217d..bba66d2b32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,5 @@ name: CI -# yamllint disable-line rule:truthy on: push: branches: @@ -8,379 +7,12 @@ on: - master pull_request: ~ -env: - CACHE_VERSION: 1 - PYTHON_VERSION_DEFAULT: '3.9.15' - PRE_COMMIT_HOME: ~/.cache/pre-commit - jobs: - # Separate job to pre-populate the base dependency cache - # This prevent upcoming jobs to do the same individually - prepare-base: - name: Prepare base dependencies - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.8.14', '3.9.15', '3.10.8', '3.11.0'] - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- - - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - python -m venv venv - . venv/bin/activate - pip install -U pip setuptools pre-commit - pip install -r requirements_test_all.txt - pip install -e . - - pre-commit: - name: Prepare pre-commit environment - runs-on: ubuntu-latest - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v3 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- - - name: Install pre-commit dependencies - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - . venv/bin/activate - pre-commit install-hooks - - lint-black: - name: Check black - runs-on: ubuntu-latest - needs: pre-commit - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v3 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Run black - run: | - . venv/bin/activate - pre-commit run --hook-stage manual black --all-files --show-diff-on-failure - - lint-flake8: - name: Check flake8 - runs-on: ubuntu-latest - needs: pre-commit - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v3 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Register flake8 problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/flake8.json" - - name: Run flake8 - run: | - . venv/bin/activate - pre-commit run --hook-stage manual flake8 --all-files - - lint-isort: - name: Check isort - runs-on: ubuntu-latest - needs: pre-commit - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v3 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Run isort - run: | - . venv/bin/activate - pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure - - lint-codespell: - name: Check codespell - runs-on: ubuntu-latest - needs: pre-commit - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v3 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Register codespell problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/codespell.json" - - name: Run codespell - run: | - . venv/bin/activate - pre-commit run --hook-stage manual codespell --all-files --show-diff-on-failure - - pytest: - runs-on: ubuntu-latest - needs: prepare-base - strategy: - matrix: - python-version: ['3.8.14', '3.9.15', '3.10.8', '3.11.0'] - name: >- - Run tests Python ${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ matrix.python-version }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Register Python problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Install Pytest Annotation plugin - run: | - . venv/bin/activate - # Ideally this should be part of our dependencies - # However this plugin is fairly new and doesn't run correctly - # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures - - name: Run pytest - run: | - . venv/bin/activate - pytest \ - -qq \ - --timeout=9 \ - --durations=10 \ - --cov zhaquirks \ - -o console_output_style=count \ - -p no:sugar \ - tests - - name: Upload coverage artifact - uses: actions/upload-artifact@v3 - with: - name: coverage-${{ matrix.python-version }} - path: .coverage - - name: Coveralls - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: ${{ matrix.python-version }} - COVERALLS_PARALLEL: true - run: | - . venv/bin/activate - coveralls --service=github - - coverage: - name: Process test coverage - runs-on: ubuntu-latest - needs: pytest - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.PYTHON_VERSION_DEFAULT }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('setup.py') }}-${{ - hashFiles('requirements_test_all.txt') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Download all coverage artifacts - uses: actions/download-artifact@v3 - - name: Combine coverage results - run: | - . venv/bin/activate - coverage combine coverage*/.coverage* - coverage report --fail-under=72 - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - - name: Upload coverage to Coveralls - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - . venv/bin/activate - coveralls --finish + shared-ci: + uses: zigpy/workflows/.github/workflows/ci.yml@main + with: + CODE_FOLDER: zhaquirks + CACHE_VERSION: 2 + PYTHON_VERSION_DEFAULT: 3.9.15 + PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit + MINIMUM_COVERAGE_PERCENTAGE: 80 diff --git a/.github/workflows/matchers/ruff.json b/.github/workflows/matchers/ruff.json new file mode 100644 index 0000000000..4411fc5e2b --- /dev/null +++ b/.github/workflows/matchers/ruff.json @@ -0,0 +1,30 @@ +{ + "problemMatcher": [ + { + "owner": "ruff-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + }, + { + "owner": "ruff-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] + } \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6dd5700c83..919c634c6a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,29 +1,26 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v3.3.1 hooks: - id: pyupgrade + args: [--py38-plus] - - repo: https://github.com/fsouza/autoflake8 - rev: v0.3.1 + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.2 hooks: - - id: autoflake8 + - id: autoflake - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.3.0 hooks: - id: black args: - - --safe - --quiet - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 + rev: 6.0.0 hooks: - id: flake8 - additional_dependencies: - - flake8-docstrings==1.6.0 - - pydocstyle==6.1.1 - repo: https://github.com/PyCQA/isort rev: 5.12.0 @@ -31,11 +28,18 @@ repos: - id: isort - repo: https://github.com/codespell-project/codespell - rev: v2.1.0 + rev: v2.2.4 hooks: - id: codespell - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v1.2.0 hooks: - id: mypy + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.261 + hooks: + - id: ruff + args: + - --fix \ No newline at end of file diff --git a/requirements_test_all.txt b/requirements_test.txt similarity index 92% rename from requirements_test_all.txt rename to requirements_test.txt index 0970e70214..32f325bc5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test.txt @@ -14,3 +14,4 @@ pytest-timeout pytest-asyncio pytest>=7.1.3 zigpy>=0.53 +ruff==0.0.261 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000000..178e0c1391 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,71 @@ +target-version = "py38" + +select = [ + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "ICN001", # import concentions; {name} should be imported as {asname} + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T20", # flake8-print + "TRY004", # Prefer TypeError exception for invalid type + "RUF006", # Store a reference to the return value of asyncio.create_task + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D205", # 1 blank line required between summary line and description + "D213", # Multi-line docstring summary should start at the second line + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood: + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D415", # First line should end with a period, question mark, or exclamation point + "E501", # line too long + # the rules below this line should be corrected + "PGH004", # Use specific rule codes when using `noqa` +] + +extend-exclude = [ + "tests" +] + +[flake8-pytest-style] +fixture-parentheses = false + +[pyupgrade] +keep-runtime-typing = true + +[isort] +# will group `import x` and `from x import` of the same module. +force-sort-within-sections = true +known-first-party = [ + "zhaquirks", + "tests", +] +forced-separate = ["tests"] +combine-as-imports = true + +[mccabe] +max-complexity = 25 \ No newline at end of file diff --git a/script/setup b/script/setup index 59c059f8cf..de03cd32fa 100755 --- a/script/setup +++ b/script/setup @@ -17,7 +17,7 @@ fi python3 -m venv venv source venv/bin/activate -pip install -r requirements_test_all.txt +pip install -r requirements_test.txt pre-commit install python3 -m pip install -e . diff --git a/tests/test_inovelli_blue.py b/tests/test_inovelli_blue.py index 8b5f829dcc..bddb2bf90e 100644 --- a/tests/test_inovelli_blue.py +++ b/tests/test_inovelli_blue.py @@ -14,7 +14,6 @@ def test_mfg_cluster_events(zigpy_device_from_quirk): endpoint_id = 2 class Listener: - zha_send_event = mock.MagicMock() device = zigpy_device_from_quirk(InovelliVZM31SNv11) diff --git a/tests/test_tuya.py b/tests/test_tuya.py index 67471a1b4d..4316df6b91 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -240,7 +240,6 @@ async def test_singleswitch_requests(zigpy_device_from_quirk, quirk): with mock.patch.object( tuya_cluster.endpoint, "request", return_value=foundation.Status.SUCCESS ) as m1: - rsp = await switch_cluster.command(0x0000) await wait_for_zigpy_tasks() m1.assert_called_with( @@ -367,7 +366,6 @@ async def async_success(*args, **kwargs): with mock.patch.object( tuya_cluster.endpoint, "request", side_effect=async_success ) as m1: - (status,) = await tuya_cluster.write_attributes({617: 179}) m1.assert_called_with( 61184, @@ -434,7 +432,6 @@ async def async_success(*args, **kwargs): with mock.patch.object( tuya_cluster.endpoint, "request", side_effect=async_success ) as m1: - _, status = await switch_cluster.command(0x0000) m1.assert_called_with( 61184, @@ -517,7 +514,6 @@ async def async_success(*args, **kwargs): with mock.patch.object( tuya_cluster.endpoint, "request", side_effect=async_success ) as m1: - (status,) = await thermostat_cluster.write_attributes( { "occupied_heating_setpoint": 2500, @@ -647,7 +643,6 @@ async def async_success(*args, **kwargs): with mock.patch.object( tuya_cluster.endpoint, "request", side_effect=async_success ) as m1: - (status,) = await thermostat_cluster.write_attributes( { "occupied_heating_setpoint": 2500, @@ -924,7 +919,6 @@ async def async_success(*args, **kwargs): with mock.patch.object( tuya_cluster.endpoint, "request", side_effect=async_success ) as m1: - (status,) = await thermostat_cluster.write_attributes( { "occupied_heating_setpoint": 2500, @@ -1291,7 +1285,6 @@ async def async_success(*args, **kwargs): with mock.patch.object( tuya_cluster.endpoint, "request", side_effect=async_success ) as m1: - (status,) = await thermostat_cluster.write_attributes( { "occupied_heating_setpoint": 2500, @@ -1631,7 +1624,6 @@ async def test_tuya_spell(zigpy_device_from_quirk): device.endpoints[1].in_clusters.values(), device.endpoints[1].out_clusters.values(), ): - # emulate ZHA calling bind() on most default clusters with an unchanged ep_attribute if ( not isinstance(cluster, int) diff --git a/tests/test_tuya_dimmer.py b/tests/test_tuya_dimmer.py index d1dfe42fa6..943d5a597b 100644 --- a/tests/test_tuya_dimmer.py +++ b/tests/test_tuya_dimmer.py @@ -127,7 +127,6 @@ async def test_write_attr(zigpy_device_from_quirk, quirk): with mock.patch.object( tuya_cluster.endpoint, "request", return_value=foundation.Status.SUCCESS ) as m1: - (status,) = await dimmer1_cluster.write_attributes( { "minimum_level": 25, diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index e46f0199c8..dcab84e5a6 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -683,7 +683,6 @@ async def test_aqara_feeder_attr_reports( """Test Aqara C1 pet feeder attr writing.""" class Listener: - attribute_updated = mock.MagicMock() device = zigpy_device_from_quirk(AqaraFeederAcn001) @@ -851,7 +850,9 @@ def mock_read(attributes, manufacturer=None): ), ) - with patch_opple_read, patch_thermostat_read, patch_opple_write, patch_thermostat_write: + with ( + patch_opple_read + ), patch_thermostat_read, patch_opple_write, patch_thermostat_write: # test reads: # read system_mode attribute from thermostat cluster diff --git a/tox.ini b/tox.ini index c3502b1923..6d1f688b2e 100644 --- a/tox.ini +++ b/tox.ini @@ -7,12 +7,12 @@ setenv = PYTHONPATH = {toxinidir} install_command = pip install {opts} {packages} commands = py.test --cov --cov-report= --timeout=9 --durations=10 -qq -p no:sugar {posargs} deps = - -r{toxinidir}/requirements_test_all.txt + -r{toxinidir}/requirements_test.txt [testenv:pylint] ignore_errors = True deps = - -r{toxinidir}/requirements_test_all.txt + -r{toxinidir}/requirements_test.txt commands = pylint {posargs} zhaquirks diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index a9027a3a1d..11b85c46f2 100644 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -6,7 +6,7 @@ import logging import pathlib import pkgutil -from typing import Any, Dict, List, Optional, Union +from typing import Any import zigpy.device import zigpy.endpoint @@ -108,11 +108,10 @@ class EventableCluster(CustomCluster): def handle_cluster_request( self, hdr: foundation.ZCLHeader, - args: List[Any], + args: list[Any], *, - dst_addressing: Optional[ - Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK] - ] = None, + dst_addressing: None + | (t.Addressing.Group | t.Addressing.IEEE | t.Addressing.NWK) = None, ): """Send cluster requests as events.""" if ( @@ -259,11 +258,10 @@ class MotionWithReset(_Motion): def handle_cluster_request( self, hdr: foundation.ZCLHeader, - args: List[Any], + args: list[Any], *, - dst_addressing: Optional[ - Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK] - ] = None, + dst_addressing: None + | (t.Addressing.Group | t.Addressing.IEEE | t.Addressing.NWK) = None, ): """Handle the cluster command.""" # check if the command is for a zone status change of ZoneStatus.Alarm_1 or ZoneStatus.Alarm_2 @@ -349,11 +347,11 @@ def _update_attribute(self, attrid, value): class QuickInitDevice(CustomDevice): """Devices with quick initialization from quirk signature.""" - signature: Optional[Dict[str, Any]] = None + signature: dict[str, Any] | None = None @classmethod def from_signature( - cls, device: zigpy.device.Device, model: Optional[str] = None + cls, device: zigpy.device.Device, model: str | None = None ) -> zigpy.device.Device: """Update device accordingly to quirk signature.""" @@ -396,7 +394,7 @@ def setup(custom_quirks_path: str | None = None) -> None: """Register all quirks with zigpy, including optional custom quirks.""" # Import all quirks in the `zhaquirks` package first - for importer, modname, _ispkg in pkgutil.walk_packages( + for _importer, modname, _ispkg in pkgutil.walk_packages( path=__path__, prefix=__name__ + ".", ): diff --git a/zhaquirks/eurotronic/__init__.py b/zhaquirks/eurotronic/__init__.py index b67479a02b..7585298a82 100644 --- a/zhaquirks/eurotronic/__init__.py +++ b/zhaquirks/eurotronic/__init__.py @@ -92,7 +92,6 @@ async def read_attributes_raw(self, attributes, manufacturer=None): success.append(rar) if OCCUPIED_HEATING_SETPOINT_ATTR in attributes: - _LOGGER.debug("intercepting OCC_HS") values = await super().read_attributes_raw( @@ -135,7 +134,6 @@ async def read_attributes_raw(self, attributes, manufacturer=None): def write_attributes(self, attributes, manufacturer=None): """Override wrong writes to thermostat attributes.""" if "system_mode" in attributes: - host_flags = self._attr_cache.get(HOST_FLAGS_ATTR, 1) _LOGGER.debug("current host_flags: %s", host_flags) diff --git a/zhaquirks/kof/kof_mr101z.py b/zhaquirks/kof/kof_mr101z.py index 18f932c251..5918388705 100644 --- a/zhaquirks/kof/kof_mr101z.py +++ b/zhaquirks/kof/kof_mr101z.py @@ -56,7 +56,7 @@ async def command(self, command, *args, expect_reply=None, **kwargs): else: cmd_expect_reply = expect_reply - rsp = await super(NoReplyMixin, self).command( + rsp = await super().command( command, *args, expect_reply=cmd_expect_reply, **kwargs ) diff --git a/zhaquirks/samjin/__init__.py b/zhaquirks/samjin/__init__.py index 694206a827..f1c4e19682 100644 --- a/zhaquirks/samjin/__init__.py +++ b/zhaquirks/samjin/__init__.py @@ -39,5 +39,5 @@ def handle_cluster_request( COMMAND_ID: hdr.command_id, ARGS: args, } - action = "button_{}".format(CLICK_TYPES[state]) + action = f"button_{CLICK_TYPES[state]}" self.listener_event(ZHA_SEND_EVENT, action, event_args) diff --git a/zhaquirks/smartwings/wm25lz.py b/zhaquirks/smartwings/wm25lz.py index 273af66130..a394e92ddb 100644 --- a/zhaquirks/smartwings/wm25lz.py +++ b/zhaquirks/smartwings/wm25lz.py @@ -29,7 +29,7 @@ class InvertedWindowCoveringCluster(CustomCluster, WindowCovering): - """This WindowCovering cluster implementation inverts the commands for up and down.""" + """WindowCovering cluster implementation that inverts the commands for up and down.""" CMD_UP_OPEN = WindowCovering.commands_by_name["up_open"].id CMD_DOWN_CLOSE = WindowCovering.commands_by_name["down_close"].id diff --git a/zhaquirks/terncy/__init__.py b/zhaquirks/terncy/__init__.py index 5b305ea368..3e2e53a94e 100644 --- a/zhaquirks/terncy/__init__.py +++ b/zhaquirks/terncy/__init__.py @@ -168,7 +168,7 @@ def handle_cluster_request( if state > 5: state = 5 event_args = {PRESS_TYPE: CLICK_TYPES[state], "count": count, VALUE: state} - action = "button_{}".format(CLICK_TYPES[state]) + action = f"button_{CLICK_TYPES[state]}" self.listener_event(ZHA_SEND_EVENT, action, event_args) elif hdr.command_id == 4: # motion event state = args[2] diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index a6d90e079d..ddc7e0f136 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -50,7 +50,7 @@ TUYA_MCU_COMMAND = "tuya_mcu_command" # Rotating for remotes -STOP = "stop" # To constans +STOP = "stop" # To constants # --------------------------------------------------------- # Value for dp_type @@ -216,7 +216,7 @@ def __init__(self, value=None, function=0, *args, **kwargs): self.dp_type = TuyaDPType.BITMAP elif isinstance(value, (bool, t.Bool)): self.dp_type = TuyaDPType.BOOL - elif isinstance(value, enum.Enum): + elif isinstance(value, enum.Enum): # type: ignore # noqa self.dp_type = TuyaDPType.ENUM elif isinstance(value, int): self.dp_type = TuyaDPType.VALUE diff --git a/zhaquirks/tuya/mcu/__init__.py b/zhaquirks/tuya/mcu/__init__.py index 4f91503bab..6c70e91caf 100644 --- a/zhaquirks/tuya/mcu/__init__.py +++ b/zhaquirks/tuya/mcu/__init__.py @@ -103,7 +103,6 @@ async def write_attributes(self, attributes, manufacturer=None): records = self._write_attr_records(attributes) for record in records: - self.debug("write_attributes --> record: %s", record) cluster_data = TuyaClusterData( @@ -148,7 +147,7 @@ def version(self) -> str: minor = (self.version_raw & 63) >> 4 release = self.version_raw & 15 - return "{}.{}.{}".format(major, minor, release) + return f"{major}.{minor}.{release}" return None diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 5c0932b6f0..07006c26b6 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -22,8 +22,6 @@ class TuyaInWallLevelControlNM(NoManufacturerCluster, TuyaInWallLevelControl): """Tuya Level cluster for inwall dimmable device with NoManufacturerID.""" - pass - # --- DEVICE SUMMARY --- # TuyaSingleSwitchDimmer: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 diff --git a/zhaquirks/tuya/ts0601_trv.py b/zhaquirks/tuya/ts0601_trv.py index 1e9b5bcd84..9f81bacde8 100644 --- a/zhaquirks/tuya/ts0601_trv.py +++ b/zhaquirks/tuya/ts0601_trv.py @@ -515,7 +515,6 @@ def map_attribute(self, attribute, value): if attribute in self.WORKDAY_SCHEDULE_ATTRS: data = data144() for num, (attr, default) in enumerate(self.WORKDAY_SCHEDULE_ATTRS.items()): - if num % 3 == 0: if attr == attribute: val = round(value / 100) @@ -539,7 +538,6 @@ def map_attribute(self, attribute, value): if attribute in self.WEEKEND_SCHEDULE_ATTRS: data = data144() for num, (attr, default) in enumerate(self.WEEKEND_SCHEDULE_ATTRS.items()): - if num % 3 == 0: if attr == attribute: val = round(value / 100) @@ -874,7 +872,6 @@ async def command( """Override the default Cluster command.""" if command_id in (0x0000, 0x0001, 0x0002): - if command_id == 0x0000: value = False elif command_id == 0x0001: @@ -1257,7 +1254,6 @@ async def command( """Override the default Cluster command.""" if command_id in (0x0000, 0x0001, 0x0002): - if command_id == 0x0000: value = False elif command_id == 0x0001: diff --git a/zhaquirks/tuya/ts0601_valve.py b/zhaquirks/tuya/ts0601_valve.py index 54e28e6f43..d37911b92f 100644 --- a/zhaquirks/tuya/ts0601_valve.py +++ b/zhaquirks/tuya/ts0601_valve.py @@ -188,8 +188,7 @@ class ParksideTuyaValveManufCluster(TuyaMCUCluster): } async def bind(self): - """ - Bind cluster. + """Bind cluster. When adding this device tuya gateway issues factory reset, we just need to reset the frost lock, because its state is unknown to us. diff --git a/zhaquirks/xbee/__init__.py b/zhaquirks/xbee/__init__.py index 5280e04500..98068a372e 100644 --- a/zhaquirks/xbee/__init__.py +++ b/zhaquirks/xbee/__init__.py @@ -447,9 +447,7 @@ def handle_cluster_request( status = ATCommandResult.ERROR if status: - fut.set_exception( - RuntimeError("AT Command response: {}".format(status.name)) - ) + fut.set_exception(RuntimeError(f"AT Command response: {status.name}")) return response_type = AT_COMMANDS[args.cmd.decode("ascii")] diff --git a/zhaquirks/xbee/types.py b/zhaquirks/xbee/types.py index 7a54c56cc9..65ceabc5b2 100644 --- a/zhaquirks/xbee/types.py +++ b/zhaquirks/xbee/types.py @@ -1,4 +1,5 @@ """Types used to serialize and deserialize XBee commands.""" +from __future__ import annotations class Bytes(bytes): @@ -71,7 +72,7 @@ def deserialize(cls, data): ] analog_pins = list(reversed(analog_pins)) if 1 in digital_pins: - digital_samples = [ + digital_samples: list[int | None] = [ (int.from_bytes(digital_sample, byteorder="big") >> bit) & 1 for bit in range(num_bits - 1, -1, -1) ] diff --git a/zhaquirks/xiaomi/aqara/cube.py b/zhaquirks/xiaomi/aqara/cube.py index 87458bb52d..66f49e5ce6 100644 --- a/zhaquirks/xiaomi/aqara/cube.py +++ b/zhaquirks/xiaomi/aqara/cube.py @@ -185,7 +185,6 @@ def _update_attribute(self, attrid, value): ) event_args = {VALUE: value} if action is not None: - if action in (SLIDE, KNOCK): event_args[DESCRIPTION] = MOVEMENT_TYPE_DESCRIPTION[value] event_args[ACTIVATED_FACE] = SIDES[value] diff --git a/zhaquirks/xiaomi/aqara/cube_aqgl01.py b/zhaquirks/xiaomi/aqara/cube_aqgl01.py index 4d3ec01ece..0e2cfa6d61 100644 --- a/zhaquirks/xiaomi/aqara/cube_aqgl01.py +++ b/zhaquirks/xiaomi/aqara/cube_aqgl01.py @@ -171,7 +171,6 @@ def _update_attribute(self, attrid, value): self._current_state[STATUS_TYPE_ATTR] = action = MOVEMENT_TYPE.get(value) event_args = {VALUE: value} if action is not None: - if action in (SLIDE, KNOCK): event_args[DESCRIPTION] = MOVEMENT_TYPE_DESCRIPTION[value] event_args[ACTIVATED_FACE] = SIDES[value] diff --git a/zhaquirks/xiaomi/aqara/motion_ac02.py b/zhaquirks/xiaomi/aqara/motion_ac02.py index 23097893a1..27085d7a5d 100644 --- a/zhaquirks/xiaomi/aqara/motion_ac02.py +++ b/zhaquirks/xiaomi/aqara/motion_ac02.py @@ -80,7 +80,7 @@ def __init__(self, *args, **kwargs): def illuminance_reported(self, value): """Illuminance reported.""" - if 0 > value or value > 0xFFDC: + if value < 0 or value > 0xFFDC: _LOGGER.debug( "Received invalid illuminance value: %s - setting illuminance to 0", value, diff --git a/zhaquirks/xiaomi/aqara/opple_remote.py b/zhaquirks/xiaomi/aqara/opple_remote.py index 6a315669fa..6161419f18 100644 --- a/zhaquirks/xiaomi/aqara/opple_remote.py +++ b/zhaquirks/xiaomi/aqara/opple_remote.py @@ -128,7 +128,7 @@ def _update_attribute(self, attrid, value): ATTR_ID: attrid, VALUE: value, } - action = "{}_{}".format(self.endpoint.endpoint_id, self._current_state) + action = f"{self.endpoint.endpoint_id}_{self._current_state}" self.listener_event(ZHA_SEND_EVENT, action, event_args) # show something in the sensor in HA super()._update_attribute(0, action) diff --git a/zhaquirks/xiaomi/aqara/opple_switch.py b/zhaquirks/xiaomi/aqara/opple_switch.py index 00d86e6010..46c2aaeaf1 100644 --- a/zhaquirks/xiaomi/aqara/opple_switch.py +++ b/zhaquirks/xiaomi/aqara/opple_switch.py @@ -109,7 +109,7 @@ def _update_attribute(self, attrid, value): ATTR_ID: attrid, VALUE: value, } - action = "{}_{}".format(self.endpoint.endpoint_id, self._current_state) + action = f"{self.endpoint.endpoint_id}_{self._current_state}" self.listener_event(ZHA_SEND_EVENT, action, event_args) # show something in the sensor in HA super()._update_attribute(0, action) diff --git a/zhaquirks/xiaomi/aqara/plug_eu.py b/zhaquirks/xiaomi/aqara/plug_eu.py index 365350e726..515779ce01 100644 --- a/zhaquirks/xiaomi/aqara/plug_eu.py +++ b/zhaquirks/xiaomi/aqara/plug_eu.py @@ -47,8 +47,7 @@ async def remove_from_ep(dev: zigpy.device.Device) -> None: - """ - Remove devices that are in group 0 by default, so IKEA devices don't control them. + """Remove devices that are in group 0 by default, so IKEA devices don't control them. This is only needed for newer firmware versions. Only a downgrade will fully fix this but this should improve it. See https://github.com/zigpy/zha-device-handlers/pull/1656#issuecomment-1244750465 for details. diff --git a/zhaquirks/xiaomi/aqara/remote_b286acn01.py b/zhaquirks/xiaomi/aqara/remote_b286acn01.py index 49f1f90694..53c9bcfbdb 100644 --- a/zhaquirks/xiaomi/aqara/remote_b286acn01.py +++ b/zhaquirks/xiaomi/aqara/remote_b286acn01.py @@ -88,7 +88,7 @@ def _update_attribute(self, attrid, value): ATTR_ID: attrid, VALUE: value, } - action = "{}_{}".format(button, self._current_state) + action = f"{button}_{self._current_state}" self.listener_event(ZHA_SEND_EVENT, action, event_args) # show something in the sensor in HA super()._update_attribute(0, action) diff --git a/zhaquirks/xiaomi/aqara/roller_curtain_e1.py b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py index 30def90775..4714286bc7 100644 --- a/zhaquirks/xiaomi/aqara/roller_curtain_e1.py +++ b/zhaquirks/xiaomi/aqara/roller_curtain_e1.py @@ -77,7 +77,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._update_attribute(0x006F, 0x00) # status_flags def _update_attribute(self, attrid: int, value: Any) -> None: - super()._update_attribute(attrid, value) if attrid == PRESENT_VALUE: From 4d01647a8db91c4732f73322b7e6b19d2e2107df Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 11 Apr 2023 03:21:56 +0200 Subject: [PATCH 2/8] Fix using removed property for `GroupBoundCluster` (#2328) --- zhaquirks/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 11b85c46f2..39b6811c95 100644 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -156,7 +156,7 @@ async def bind(self): """Bind cluster to a group.""" # Ensure coordinator is a member of the group application = self._endpoint.device.application - coordinator = application.get_device(application.ieee) + coordinator = application.get_device(application.state.node_info.ieee) await coordinator.add_to_group( self.COORDINATOR_GROUP_ID, name="Coordinator Group - Created by ZHAQuirks", From b1cdcffb63bc8d04ce8519f29ba6eb41421ebcb9 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 11 Apr 2023 03:28:17 +0200 Subject: [PATCH 3/8] Remove custom IKEA LightLinkCluster (#2326) --- zhaquirks/ikea/__init__.py | 28 ---------------------------- zhaquirks/ikea/fivebtnremote.py | 5 ++--- zhaquirks/ikea/motion.py | 4 ++-- zhaquirks/ikea/twobtnremote.py | 9 ++------- 4 files changed, 6 insertions(+), 40 deletions(-) diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py index 8a2c0a7c66..50a7bd56b2 100644 --- a/zhaquirks/ikea/__init__.py +++ b/zhaquirks/ikea/__init__.py @@ -5,7 +5,6 @@ import zigpy.types as t from zigpy.zcl import foundation from zigpy.zcl.clusters.general import PowerConfiguration, Scenes -from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks import DoublingPowerConfigurationCluster, EventableCluster @@ -28,33 +27,6 @@ ].id -class LightLinkCluster(CustomCluster, LightLink): - """Ikea LightLink cluster.""" - - async def bind(self): - """Bind LightLink cluster to coordinator.""" - application = self._endpoint.device.application - try: - coordinator = application.get_device(application.ieee) - except KeyError: - _LOGGER.warning("Aborting - unable to locate required coordinator device.") - return - group_list = await self.get_group_identifiers(0) - try: - group_record = group_list[2] - group_id = group_record[0].group_id - except IndexError: - _LOGGER.warning( - "unable to locate required group info - falling back to group 0x0000." - ) - group_id = 0x0000 - status = await coordinator.add_to_group( - group_id, - name="Default Lightlink Group", - ) - return [status] - - class ScenesCluster(CustomCluster, Scenes): """Ikea Scenes cluster.""" diff --git a/zhaquirks/ikea/fivebtnremote.py b/zhaquirks/ikea/fivebtnremote.py index 8b8f773917..eba8610882 100644 --- a/zhaquirks/ikea/fivebtnremote.py +++ b/zhaquirks/ikea/fivebtnremote.py @@ -52,7 +52,6 @@ IKEA_CLUSTER_ID, WWAH_CLUSTER_ID, DoublingPowerConfig1CRCluster, - LightLinkCluster, PowerConfig1CRCluster, ScenesCluster, ) @@ -103,7 +102,7 @@ class IkeaTradfriRemote1(CustomDevice): Identify.cluster_id, Alarms.cluster_id, Diagnostic.cluster_id, - LightLinkCluster, + LightLink.cluster_id, ], OUTPUT_CLUSTERS: [ Identify.cluster_id, @@ -309,7 +308,7 @@ class IkeaTradfriRemote3(IkeaTradfriRemote1): DoublingPowerConfig1CRCluster, Identify.cluster_id, Alarms.cluster_id, - LightLinkCluster, + LightLink.cluster_id, ], OUTPUT_CLUSTERS: [ Identify.cluster_id, diff --git a/zhaquirks/ikea/motion.py b/zhaquirks/ikea/motion.py index 2a034607d5..a7c5aa6b0d 100644 --- a/zhaquirks/ikea/motion.py +++ b/zhaquirks/ikea/motion.py @@ -21,7 +21,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.ikea import IKEA, DoublingPowerConfig2CRCluster, LightLinkCluster +from zhaquirks.ikea import IKEA, DoublingPowerConfig2CRCluster class IkeaTradfriMotion(CustomDevice): @@ -67,7 +67,7 @@ class IkeaTradfriMotion(CustomDevice): Identify.cluster_id, Alarms.cluster_id, Diagnostic.cluster_id, - LightLinkCluster, + LightLink.cluster_id, ], OUTPUT_CLUSTERS: [ Identify.cluster_id, diff --git a/zhaquirks/ikea/twobtnremote.py b/zhaquirks/ikea/twobtnremote.py index 0d217d5660..054ac23d1e 100644 --- a/zhaquirks/ikea/twobtnremote.py +++ b/zhaquirks/ikea/twobtnremote.py @@ -40,12 +40,7 @@ TURN_OFF, TURN_ON, ) -from zhaquirks.ikea import ( - IKEA, - IKEA_CLUSTER_ID, - DoublingPowerConfig1CRCluster, - LightLinkCluster, -) +from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, DoublingPowerConfig1CRCluster class IkeaTradfriRemote2Btn(CustomDevice): @@ -183,7 +178,7 @@ class IkeaTradfriRemote2BtnZLL(CustomDevice): DoublingPowerConfig1CRCluster, Identify.cluster_id, Alarms.cluster_id, - LightLinkCluster, + LightLink.cluster_id, IKEA_CLUSTER_ID, ], OUTPUT_CLUSTERS: [ From 20b9e061e69c4ada372be6f02eb1c54ebf9134eb Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 11 Apr 2023 03:33:28 +0200 Subject: [PATCH 4/8] Remove custom LDS LightLinkCluster (#2327) --- zhaquirks/lds/__init__.py | 27 --------------------------- zhaquirks/lds/cctswitch.py | 4 ++-- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/zhaquirks/lds/__init__.py b/zhaquirks/lds/__init__.py index f4f00909cd..6ff0f79ea4 100644 --- a/zhaquirks/lds/__init__.py +++ b/zhaquirks/lds/__init__.py @@ -1,29 +1,2 @@ """LDS module.""" -import logging - -from zigpy.quirks import CustomCluster -from zigpy.zcl.clusters.lightlink import LightLink - -_LOGGER = logging.getLogger(__name__) MANUFACTURER = "LDS" - - -class LightLinkCluster(CustomCluster, LightLink): - """LDS LightLink cluster.""" - - async def bind(self): - """Bind LightLink cluster to coordinator.""" - application = self._endpoint.device.application - try: - coordinator = application.get_device(application.ieee) - except KeyError: - _LOGGER.warning("Aborting - unable to locate required coordinator device.") - return - group_list = await self.get_group_identifiers(0) - group_record = group_list[2] - group_id = group_record[0].group_id - status = await coordinator.add_to_group( - group_id, - name=f"{str(self.endpoint.device.ieee)} - {self.endpoint.manufacturer} {self.endpoint.model}", - ) - return [status] diff --git a/zhaquirks/lds/cctswitch.py b/zhaquirks/lds/cctswitch.py index 43a853fe74..c666ccebfa 100644 --- a/zhaquirks/lds/cctswitch.py +++ b/zhaquirks/lds/cctswitch.py @@ -37,7 +37,7 @@ SHORT_PRESS, TURN_ON, ) -from zhaquirks.lds import MANUFACTURER, LightLinkCluster +from zhaquirks.lds import MANUFACTURER class CCTSwitch(CustomDevice): @@ -81,7 +81,7 @@ class CCTSwitch(CustomDevice): Basic.cluster_id, PowerConfiguration.cluster_id, Identify.cluster_id, - LightLinkCluster, + LightLink.cluster_id, 0xFD01, ], OUTPUT_CLUSTERS: [ From 529e63064a133e652fc54e1cb7ea65ab38a7a291 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 11 Apr 2023 16:33:04 +0200 Subject: [PATCH 5/8] Use newer Tuya spell implementation for Lidl plug (#2329) --- zhaquirks/lidl/ts011f_plug.py | 112 +--------------------------------- 1 file changed, 2 insertions(+), 110 deletions(-) diff --git a/zhaquirks/lidl/ts011f_plug.py b/zhaquirks/lidl/ts011f_plug.py index 4f97269b2c..cd8f0f0230 100644 --- a/zhaquirks/lidl/ts011f_plug.py +++ b/zhaquirks/lidl/ts011f_plug.py @@ -1,13 +1,7 @@ """LIDL TS011F plug.""" from __future__ import annotations -import asyncio -import logging - -import zigpy from zigpy.profiles import zha -from zigpy.quirks import CustomCluster -import zigpy.types as t from zigpy.zcl.clusters.general import ( Basic, GreenPowerProxy, @@ -27,111 +21,13 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) +from zhaquirks.tuya.mcu import EnchantedDevice from zhaquirks.tuya.ts011f_plug import Plug_3AC_4USB -_LOGGER = logging.getLogger(__name__) - - -async def cast_tuya_magic_spell_task( - dev: zigpy.device.Device, tries: int = 100, rejoin: bool = False -) -> None: - """Initialize device so that all endpoints become available.""" - import inspect - - basic_cluster = dev.endpoints[1].in_clusters[0] - - # The magic spell is needed only once. - # TODO: Improve by doing this only once (successfully). - - # Magic spell - part 1 - # Note: attribute order is important - attr_to_read = [ - "manufacturer", - "zcl_version", - "app_version", - "model", - "power_source", - 0xFFFE, - ] - if "tries" in inspect.getfullargspec(basic_cluster.read_attributes)[0]: - _LOGGER.debug(f"Cast Tuya Magic Spell on {dev.ieee!r} with {tries} tries") - res = await basic_cluster.read_attributes(attr_to_read, tries=tries) - else: - _LOGGER.debug(f"Cast Tuya Magic Spell on {dev.ieee!r}") - res = await basic_cluster.read_attributes(attr_to_read) - - _LOGGER.debug(f"Tuya Magic Spell result {res!r} for {dev.ieee!r}") - - # Magic spell - part 2 (skipped - does not seem to be needed) - # attr_to_write={0xffde:13} - # basic_cluster.write_attributes(attr_to_write) - - if rejoin: - # Leave with rejoin - may need to be adjuste to work everywhere - # or require a minimum zigpy version - # This should have the device leave and rejoin immediately triggering - # the discovery of the endpoints that appear after the magic trick - - # Note: this is not validated yet and disabled by default - _LOGGER.debug(f"Send leave with rejoin request to {dev.ieee!r}") - res = await dev.zdo.request(0x0034, dev.ieee, 0x01, tries) - _LOGGER.debug(f"Leave with rejoin result {res!r} for {dev.ieee!r}") - - app = dev.application - # Delete the device from the database - app.listener_event("device_removed", dev) - - # Delete the device from zigpy - app.devices.pop(dev.ieee, None) - -def cast_tuya_magic_spell(dev: zigpy.device.Device, tries: int = 3) -> None: - """Set up the magic spell asynchronously.""" - - # Note for sleepy devices the number of tries may need to be increased to 100. - - dev._magic_spell_task = asyncio.create_task( - cast_tuya_magic_spell_task(dev, tries=tries) - ) - - -class TuyaBasicCluster(CustomCluster, Basic): - """Provide Tuya Basic Cluster with magic spell.""" - - attributes = Basic.attributes.copy() - attributes.update( - { - 0xFFDE: ("tuya_FFDE", t.uint8_t, True), - # 0xffe0: ("tuya_FFE0", TODO.Array, True), - # 0xffe1: ("tuya_FFE1", TODO.Array, True), - 0xFFE2: ("tuya_FFE2", t.uint8_t, True), - # 0xffe3: ("tuya_FFE3", TODO.Array, True), - } - ) - - async def bind(self): - """Bind cluster.""" - - _LOGGER.debug( - f"Requesting Tuya Magic Spell for {self.ieee!r} in basic bind method" - ) - tries = 3 - await asyncio.create_task(cast_tuya_magic_spell_task(self, tries=tries)) - - return await super().bind() - - -class Lidl_Plug_3AC_4USB(Plug_3AC_4USB): +class Lidl_Plug_3AC_4USB(Plug_3AC_4USB, EnchantedDevice): """LIDL 3 outlets + 4 USB with restore power state support.""" - def __init__(self, *args, **kwargs): - """Initialize with task.""" - super().__init__(*args, **kwargs) - - # Use 'external' version that could be called from cluster - # customiation - cast_tuya_magic_spell(self, tries=3) - signature = { MODEL: "TS011F", ENDPOINTS: { @@ -163,7 +59,3 @@ def __init__(self, *args, **kwargs): }, }, } - - # Uncomment to try TuyaBasicCluster implementation - # Rename __init__ to disabled__init__ as well - # replacement[1][INPUT_CLUSTERS][0] = TuyaBasicCluster From fb5d256299dd789536034ca33a91afc646a07251 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 11 Apr 2023 16:38:37 +0200 Subject: [PATCH 6/8] Ignore missing default reply for GLEDOPTO AC dimmer (#2330) * Use `NoReplyMixin` for GLEDOPTO dimmer * Move `NoReplyMixin` to main `__init__.py` file --- zhaquirks/__init__.py | 38 +++++++++++++++++ zhaquirks/gledopto/glsd001.py | 77 +++++++++++++++++++++++++++++++++++ zhaquirks/kof/kof_mr101z.py | 40 +----------------- 3 files changed, 116 insertions(+), 39 deletions(-) create mode 100644 zhaquirks/gledopto/glsd001.py diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 39b6811c95..b4ebdb0ed0 100644 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -390,6 +390,44 @@ def from_signature( return device +class NoReplyMixin: + """A simple mixin. + + Allows a cluster to have configurable list of command + ids that do not generate an explicit reply. + """ + + void_input_commands: set[int] = {} + + async def command(self, command, *args, expect_reply=None, **kwargs): + """Override the default Cluster command. + + expect_reply behavior is based on void_input_commands. + Note that this method changes the default value of + expect_reply to None. This allows the caller to explicitly force + expect_reply to true. + """ + + if expect_reply is None and command in self.void_input_commands: + cmd_expect_reply = False + elif expect_reply is None: + cmd_expect_reply = True # the default + else: + cmd_expect_reply = expect_reply + + rsp = await super().command( + command, *args, expect_reply=cmd_expect_reply, **kwargs + ) + + if expect_reply is None and command in self.void_input_commands: + # Pretend we received a default reply + return foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Default_Response + ].schema(command_id=command, status=foundation.Status.SUCCESS) + + return rsp + + def setup(custom_quirks_path: str | None = None) -> None: """Register all quirks with zigpy, including optional custom quirks.""" diff --git a/zhaquirks/gledopto/glsd001.py b/zhaquirks/gledopto/glsd001.py new file mode 100644 index 0000000000..4f9d31c9c5 --- /dev/null +++ b/zhaquirks/gledopto/glsd001.py @@ -0,0 +1,77 @@ +"""Quirk for GLEDOPTO GL-SD-001.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks import NoReplyMixin +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class LevelControlNoReply(NoReplyMixin, CustomCluster, LevelControl): + """LevelControl cluster that does not require default responses.""" + + void_input_commands = {cmd.id for cmd in LevelControl.commands_by_name.values()} + + +class GledoptoGlSd001(CustomDevice): + """Gledopto GL-SD-001 dimmer custom device implementation.""" + + signature = { + MODELS_INFO: [("GLEDOPTO", "GL-SD-001")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControlNoReply, + LightLink.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } diff --git a/zhaquirks/kof/kof_mr101z.py b/zhaquirks/kof/kof_mr101z.py index 5918388705..08cba1e3ea 100644 --- a/zhaquirks/kof/kof_mr101z.py +++ b/zhaquirks/kof/kof_mr101z.py @@ -9,7 +9,6 @@ from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( Basic, Groups, @@ -21,6 +20,7 @@ ) from zigpy.zcl.clusters.hvac import Fan +from zhaquirks import NoReplyMixin from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -31,44 +31,6 @@ ) -class NoReplyMixin: - """A simple mixin. - - Allows a cluster to have configurable list of command - ids that do not generate an explicit reply. - """ - - void_input_commands: set[int] = {} - - async def command(self, command, *args, expect_reply=None, **kwargs): - """Override the default Cluster command. - - expect_reply behavior is based on void_input_commands. - Note that this method changes the default value of - expect_reply to None. This allows the caller to explicitly force - expect_reply to true. - """ - - if expect_reply is None and command in self.void_input_commands: - cmd_expect_reply = False - elif expect_reply is None: - cmd_expect_reply = True # the default - else: - cmd_expect_reply = expect_reply - - rsp = await super().command( - command, *args, expect_reply=cmd_expect_reply, **kwargs - ) - - if expect_reply is None and command in self.void_input_commands: - # Pretend we received a default reply - return foundation.GENERAL_COMMANDS[ - foundation.GeneralCommand.Default_Response - ].schema(command_id=command, status=foundation.Status.SUCCESS) - - return rsp - - class KofBasic(NoReplyMixin, CustomCluster, Basic): """KOF Basic Cluster.""" From f2f01b0c93956de270a331c0c6171524c205b3de Mon Sep 17 00:00:00 2001 From: codyhackw <49957005+codyhackw@users.noreply.github.com> Date: Tue, 11 Apr 2023 21:20:58 -0400 Subject: [PATCH 7/8] Updates for Inovelli's 2.14 Firmware (#2334) * Update __init__.py * Update __init__.py Slight name change for clarity --- zhaquirks/inovelli/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zhaquirks/inovelli/__init__.py b/zhaquirks/inovelli/__init__.py index b2fe118e6d..84f3cc2ff5 100644 --- a/zhaquirks/inovelli/__init__.py +++ b/zhaquirks/inovelli/__init__.py @@ -108,11 +108,14 @@ class Inovelli_VZM31SN_Cluster(CustomCluster): 0x0014: ("active_energy_reports", t.uint16_t, True), 0x0015: ("power_type", t.uint8_t, True), 0x0016: ("switch_type", t.uint8_t, True), + 0x0019: ("increased_non_neutral_output", t.Bool, True), 0x0032: ("button_delay", t.uint8_t, True), 0x0033: ("device_bind_number", t.uint8_t, True), 0x0034: ("smart_bulb_mode", t.Bool, True), - 0x0035: ("double_tap_up_for_max_brightness", t.Bool, True), - 0x0036: ("double_tap_down_for_min_brightness", t.Bool, True), + 0x0035: ("double_tap_up_enabled", t.Bool, True), + 0x0036: ("double_tap_down_enabled", t.Bool, True), + 0x0037: ("double_tap_up_level", t.uint8_t, True), + 0x0038: ("double_tap_down_level", t.uint8_t, True), 0x003C: ("default_led1_strip_color_when_on", t.uint8_t, True), 0x003D: ("default_led1_strip_color_when_off", t.uint8_t, True), 0x003E: ("default_led1_strip_intensity_when_on", t.uint8_t, True), @@ -145,6 +148,9 @@ class Inovelli_VZM31SN_Cluster(CustomCluster): 0x0060: ("led_color_when_off", t.uint8_t, True), 0x0061: ("led_intensity_when_on", t.uint8_t, True), 0x0062: ("led_intensity_when_off", t.uint8_t, True), + 0x0064: ("led_scaling_mode", t.Bool, True), + 0x007B: ("aux_switch_scenes", t.Bool, True), + 0x007D: ("binding_off_to_on_sync_level", t.Bool, True), 0x0100: ("local_protection", t.Bool, True), 0x0101: ("remote_protection", t.Bool, True), 0x0102: ("output_mode", t.Bool, True), From de21b51c1da43216caa2b68b97a023302db94d4f Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 11 Apr 2023 21:25:17 -0400 Subject: [PATCH 8/8] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bc00e30c82..26ba74d143 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.96" +VERSION = "0.0.97" setup(