diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 2a667f83daa..c848ac793af 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -27,7 +27,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 @@ -90,7 +90,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' @@ -242,7 +242,7 @@ jobs: - green steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set build additional args run: | @@ -279,7 +279,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -321,7 +321,7 @@ jobs: registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Install Cosign uses: sigstore/cosign-installer@v3.9.2 @@ -454,7 +454,7 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.6.0 @@ -499,7 +499,7 @@ jobs: HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }} steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Login to GitHub Container Registry uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index aca149bf020..a75121fff68 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 4 + CACHE_VERSION: 7 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.9" @@ -94,7 +94,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Generate partial Python venv restore key id: generate_python_cache_key run: | @@ -246,7 +246,7 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -255,7 +255,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: venv key: >- @@ -271,7 +271,7 @@ jobs: uv pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true @@ -292,7 +292,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.6.0 id: python @@ -301,7 +301,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -310,7 +310,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -332,7 +332,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.6.0 id: python @@ -341,7 +341,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -350,7 +350,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -372,7 +372,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.6.0 id: python @@ -381,7 +381,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -390,7 +390,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -462,7 +462,7 @@ jobs: - script/hassfest/docker/Dockerfile steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -481,7 +481,7 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.6.0 @@ -497,7 +497,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: venv key: >- @@ -505,7 +505,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore uv wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: ${{ env.UV_CACHE_DIR }} key: >- @@ -584,7 +584,7 @@ jobs: sudo apt-get -y install \ libturbojpeg - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -593,7 +593,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -617,7 +617,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -626,7 +626,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -651,9 +651,9 @@ jobs: && github.event_name == 'pull_request' steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Dependency review - uses: actions/dependency-review-action@v4.7.1 + uses: actions/dependency-review-action@v4.7.2 with: license-check: false # We use our own license audit checks @@ -674,7 +674,7 @@ jobs: python-version: ${{ fromJson(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.6.0 @@ -683,7 +683,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -717,7 +717,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -726,7 +726,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -764,7 +764,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -773,7 +773,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -809,7 +809,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -825,7 +825,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -833,7 +833,7 @@ jobs: ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore mypy cache - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: .mypy_cache key: >- @@ -886,7 +886,7 @@ jobs: libturbojpeg \ libgammu-dev - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -895,7 +895,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -947,7 +947,7 @@ jobs: libgammu-dev \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.6.0 @@ -956,7 +956,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1080,7 +1080,7 @@ jobs: libmariadb-dev-compat \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.6.0 @@ -1089,7 +1089,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1222,7 +1222,7 @@ jobs: sudo apt-get -y install \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.6.0 @@ -1231,7 +1231,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1334,14 +1334,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Download all coverage artifacts uses: actions/download-artifact@v5.0.0 with: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@v5.4.3 + uses: codecov/codecov-action@v5.5.0 with: fail_ci_if_error: true flags: full-suite @@ -1381,7 +1381,7 @@ jobs: libgammu-dev \ libxml2-utils - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v5.6.0 @@ -1390,7 +1390,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv fail-on-cache-miss: true @@ -1484,14 +1484,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Download all coverage artifacts uses: actions/download-artifact@v5.0.0 with: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@v5.4.3 + uses: codecov/codecov-action@v5.5.0 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c5dcf19ce6e..28cdd83a198 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,14 +21,14 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.29.5 + uses: github/codeql-action/init@v3.29.11 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.29.5 + uses: github/codeql-action/analyze@v3.29.11 with: category: "/language:python" diff --git a/.github/workflows/detect-duplicate-issues.yml b/.github/workflows/detect-duplicate-issues.yml index 17777f576de..5f9522e0593 100644 --- a/.github/workflows/detect-duplicate-issues.yml +++ b/.github/workflows/detect-duplicate-issues.yml @@ -231,7 +231,7 @@ jobs: - name: Detect duplicates using AI id: ai_detection if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' - uses: actions/ai-inference@v1.2.8 + uses: actions/ai-inference@v2.0.0 with: model: openai/gpt-4o system-prompt: | diff --git a/.github/workflows/detect-non-english-issues.yml b/.github/workflows/detect-non-english-issues.yml index 1aa51492c74..bcad5726968 100644 --- a/.github/workflows/detect-non-english-issues.yml +++ b/.github/workflows/detect-non-english-issues.yml @@ -57,7 +57,7 @@ jobs: - name: Detect language using AI id: ai_language_detection if: steps.detect_language.outputs.should_continue == 'true' - uses: actions/ai-inference@v1.2.8 + uses: actions/ai-inference@v2.0.0 with: model: openai/gpt-4o-mini system-prompt: | diff --git a/.github/workflows/restrict-task-creation.yml b/.github/workflows/restrict-task-creation.yml index 0a6be15180b..36d9688f50a 100644 --- a/.github/workflows/restrict-task-creation.yml +++ b/.github/workflows/restrict-task-creation.yml @@ -9,7 +9,7 @@ jobs: check-authorization: runs-on: ubuntu-latest # Only run if this is a Task issue type (from the issue form) - if: github.event.issue.issue_type == 'Task' + if: github.event.issue.type.name == 'Task' steps: - name: Check if user is authorized uses: actions/github-script@v7 diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 8a668d548d3..004b552cab3 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.6.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3f0c0d578a9..883cc688cf5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -32,7 +32,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python @@ -135,7 +135,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Download env_file uses: actions/download-artifact@v5.0.0 @@ -184,7 +184,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Download env_file uses: actions/download-artifact@v5.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 610fed902ad..d87187b55be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: exclude_types: [csv, json, html] exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-executables-have-shebangs stages: [manual] diff --git a/.strict-typing b/.strict-typing index c125e85bbfc..b3e41747239 100644 --- a/.strict-typing +++ b/.strict-typing @@ -310,7 +310,6 @@ homeassistant.components.letpot.* homeassistant.components.lidarr.* homeassistant.components.lifx.* homeassistant.components.light.* -homeassistant.components.linear_garage_door.* homeassistant.components.linkplay.* homeassistant.components.litejet.* homeassistant.components.litterrobot.* @@ -467,6 +466,7 @@ homeassistant.components.simplisafe.* homeassistant.components.siren.* homeassistant.components.skybell.* homeassistant.components.slack.* +homeassistant.components.sleep_as_android.* homeassistant.components.sleepiq.* homeassistant.components.smhi.* homeassistant.components.smlight.* diff --git a/CODEOWNERS b/CODEOWNERS index 84a07305d36..7da06479b92 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -156,8 +156,8 @@ build.json @home-assistant/supervisor /tests/components/assist_pipeline/ @balloob @synesthesiam /homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam /tests/components/assist_satellite/ @home-assistant/core @synesthesiam -/homeassistant/components/asuswrt/ @kennedyshead @ollo69 -/tests/components/asuswrt/ @kennedyshead @ollo69 +/homeassistant/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi +/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi /homeassistant/components/atag/ @MatsNL /tests/components/atag/ @MatsNL /homeassistant/components/aten_pe/ @mtdcr @@ -422,6 +422,8 @@ build.json @home-assistant/supervisor /homeassistant/components/emby/ @mezz64 /homeassistant/components/emoncms/ @borpin @alexandrecuer /tests/components/emoncms/ @borpin @alexandrecuer +/homeassistant/components/emoncms_history/ @alexandrecuer +/tests/components/emoncms_history/ @alexandrecuer /homeassistant/components/emonitor/ @bdraco /tests/components/emonitor/ @bdraco /homeassistant/components/emulated_hue/ @bdraco @Tho85 @@ -438,8 +440,8 @@ build.json @home-assistant/supervisor /tests/components/enigma2/ @autinerd /homeassistant/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer -/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac -/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac +/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac +/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac /homeassistant/components/entur_public_transport/ @hfurubotten /homeassistant/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie @@ -862,8 +864,6 @@ build.json @home-assistant/supervisor /tests/components/lifx/ @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core -/homeassistant/components/linear_garage_door/ @IceBotYT -/tests/components/linear_garage_door/ @IceBotYT /homeassistant/components/linkplay/ @Velleman /tests/components/linkplay/ @Velleman /homeassistant/components/linux_battery/ @fabaff @@ -1417,6 +1417,8 @@ build.json @home-assistant/supervisor /tests/components/skybell/ @tkdrob /homeassistant/components/slack/ @tkdrob @fletcherau /tests/components/slack/ @tkdrob @fletcherau +/homeassistant/components/sleep_as_android/ @tr4nt0r +/tests/components/sleep_as_android/ @tr4nt0r /homeassistant/components/sleepiq/ @mfugate1 @kbickar /tests/components/sleepiq/ @mfugate1 @kbickar /homeassistant/components/slide/ @ualex73 @@ -1599,6 +1601,8 @@ build.json @home-assistant/supervisor /tests/components/todo/ @home-assistant/core /homeassistant/components/todoist/ @boralyl /tests/components/todoist/ @boralyl +/homeassistant/components/togrill/ @elupus +/tests/components/togrill/ @elupus /homeassistant/components/tolo/ @MatthiasLohr /tests/components/tolo/ @MatthiasLohr /homeassistant/components/tomorrowio/ @raman325 @lymanepp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f8a79ab901..e7d8488048e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,5 +14,8 @@ Still interested? Then you should take a peek at the [developer documentation](h ## Feature suggestions -If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests). -We use [GitHub for tracking issues](https://github.com/home-assistant/core/issues), not for tracking feature requests. +If you want to suggest a new feature for Home Assistant (e.g. new integrations), please [start a discussion](https://github.com/orgs/home-assistant/discussions) on GitHub. + +## Issue Tracker + +If you want to report an issue, please [create an issue](https://github.com/home-assistant/core/issues) on GitHub. diff --git a/Dockerfile b/Dockerfile index 549837ddef0..4a004c046e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ RUN \ && go2rtc --version # Install uv -RUN pip3 install uv==0.7.1 +RUN pip3 install uv==0.8.9 WORKDIR /usr/src diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index cef4db57358..6342fa5392a 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -61,7 +61,7 @@ "display_pm_standard": { "name": "Display PM standard", "state": { - "ugm3": "µg/m³", + "ugm3": "μg/m³", "us_aqi": "US AQI" } }, diff --git a/homeassistant/components/airos/__init__.py b/homeassistant/components/airos/__init__.py index 54f0db205a9..ea184e5613d 100644 --- a/homeassistant/components/airos/__init__.py +++ b/homeassistant/components/airos/__init__.py @@ -10,7 +10,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator -_PLATFORMS: list[Platform] = [Platform.SENSOR] +_PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, +] async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: diff --git a/homeassistant/components/airos/binary_sensor.py b/homeassistant/components/airos/binary_sensor.py new file mode 100644 index 00000000000..e743cda4c63 --- /dev/null +++ b/homeassistant/components/airos/binary_sensor.py @@ -0,0 +1,106 @@ +"""AirOS Binary Sensor component for Home Assistant.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator +from .entity import AirOSEntity + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describe an AirOS binary sensor.""" + + value_fn: Callable[[AirOSData], bool] + + +BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = ( + AirOSBinarySensorEntityDescription( + key="portfw", + translation_key="port_forwarding", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.portfw, + ), + AirOSBinarySensorEntityDescription( + key="dhcp_client", + translation_key="dhcp_client", + device_class=BinarySensorDeviceClass.RUNNING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.dhcpc, + ), + AirOSBinarySensorEntityDescription( + key="dhcp_server", + translation_key="dhcp_server", + device_class=BinarySensorDeviceClass.RUNNING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.dhcpd, + entity_registry_enabled_default=False, + ), + AirOSBinarySensorEntityDescription( + key="dhcp6_server", + translation_key="dhcp6_server", + device_class=BinarySensorDeviceClass.RUNNING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.dhcp6d_stateful, + entity_registry_enabled_default=False, + ), + AirOSBinarySensorEntityDescription( + key="pppoe", + translation_key="pppoe", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.services.pppoe, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AirOSConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the AirOS binary sensors from a config entry.""" + coordinator = config_entry.runtime_data + + async_add_entities( + AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS + ) + + +class AirOSBinarySensor(AirOSEntity, BinarySensorEntity): + """Representation of a binary sensor.""" + + entity_description: AirOSBinarySensorEntityDescription + + def __init__( + self, + coordinator: AirOSDataUpdateCoordinator, + description: AirOSBinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the binary sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index 758902bbaa2..2a2a241aef0 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airos", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["airos==0.2.4"] + "requirements": ["airos==0.4.3"] } diff --git a/homeassistant/components/airos/quality_scale.yaml b/homeassistant/components/airos/quality_scale.yaml index c8c5d209af5..e8a5ce8ed89 100644 --- a/homeassistant/components/airos/quality_scale.yaml +++ b/homeassistant/components/airos/quality_scale.yaml @@ -54,9 +54,7 @@ rules: dynamic-devices: todo entity-category: done entity-device-class: done - entity-disabled-by-default: - status: todo - comment: prepared binary_sensors will provide this + entity-disabled-by-default: done entity-translations: done exception-translations: done icon-translations: diff --git a/homeassistant/components/airos/sensor.py b/homeassistant/components/airos/sensor.py index 4567261ba4d..06b06a21e28 100644 --- a/homeassistant/components/airos/sensor.py +++ b/homeassistant/components/airos/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass import logging -from airos.data import NetRole, WirelessMode +from airos.data import DerivedWirelessMode, DerivedWirelessRole, NetRole from homeassistant.components.sensor import ( SensorDeviceClass, @@ -19,6 +19,8 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS, UnitOfDataRate, UnitOfFrequency, + UnitOfLength, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -29,8 +31,11 @@ from .entity import AirOSEntity _LOGGER = logging.getLogger(__name__) -WIRELESS_MODE_OPTIONS = [mode.value.replace("-", "_").lower() for mode in WirelessMode] NETROLE_OPTIONS = [mode.value for mode in NetRole] +WIRELESS_MODE_OPTIONS = [mode.value for mode in DerivedWirelessMode] +WIRELESS_ROLE_OPTIONS = [mode.value for mode in DerivedWirelessRole] + +PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) @@ -46,6 +51,7 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = ( translation_key="host_cpuload", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, value_fn=lambda data: data.host.cpuload, entity_registry_enabled_default=False, ), @@ -83,6 +89,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, value_fn=lambda data: data.wireless.throughput.tx, ), AirOSSensorEntityDescription( @@ -91,6 +99,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, value_fn=lambda data: data.wireless.throughput.rx, ), AirOSSensorEntityDescription( @@ -99,6 +109,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, value_fn=lambda data: data.wireless.polling.dl_capacity, ), AirOSSensorEntityDescription( @@ -107,8 +119,45 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, value_fn=lambda data: data.wireless.polling.ul_capacity, ), + AirOSSensorEntityDescription( + key="host_uptime", + translation_key="host_uptime", + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + suggested_display_precision=0, + suggested_unit_of_measurement=UnitOfTime.DAYS, + value_fn=lambda data: data.host.uptime, + entity_registry_enabled_default=False, + ), + AirOSSensorEntityDescription( + key="wireless_distance", + translation_key="wireless_distance", + native_unit_of_measurement=UnitOfLength.METERS, + device_class=SensorDeviceClass.DISTANCE, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfLength.KILOMETERS, + value_fn=lambda data: data.wireless.distance, + ), + AirOSSensorEntityDescription( + key="wireless_mode", + translation_key="wireless_mode", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.derived.mode.value, + options=WIRELESS_MODE_OPTIONS, + entity_registry_enabled_default=False, + ), + AirOSSensorEntityDescription( + key="wireless_role", + translation_key="wireless_role", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda data: data.derived.role.value, + options=WIRELESS_ROLE_OPTIONS, + entity_registry_enabled_default=False, + ), ) diff --git a/homeassistant/components/airos/strings.json b/homeassistant/components/airos/strings.json index ff013862ee5..53681292f50 100644 --- a/homeassistant/components/airos/strings.json +++ b/homeassistant/components/airos/strings.json @@ -26,6 +26,23 @@ } }, "entity": { + "binary_sensor": { + "port_forwarding": { + "name": "Port forwarding" + }, + "dhcp_client": { + "name": "DHCP client" + }, + "dhcp_server": { + "name": "DHCP server" + }, + "dhcp6_server": { + "name": "DHCPv6 server" + }, + "pppoe": { + "name": "PPPoE link" + } + }, "sensor": { "host_cpuload": { "name": "CPU load" @@ -60,6 +77,26 @@ }, "wireless_remote_hostname": { "name": "Remote hostname" + }, + "host_uptime": { + "name": "Uptime" + }, + "wireless_distance": { + "name": "Wireless distance" + }, + "wireless_role": { + "name": "Wireless role", + "state": { + "access_point": "Access point", + "station": "Station" + } + }, + "wireless_mode": { + "name": "Wireless mode", + "state": { + "point_to_point": "Point-to-point", + "point_to_multipoint": "Point-to-multipoint" + } } } }, diff --git a/homeassistant/components/airq/__init__.py b/homeassistant/components/airq/__init__.py index ab64915c8ae..f87365797e7 100644 --- a/homeassistant/components/airq/__init__.py +++ b/homeassistant/components/airq/__init__.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE from .coordinator import AirQCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR] AirQConfigEntry = ConfigEntry[AirQCoordinator] diff --git a/homeassistant/components/airq/coordinator.py b/homeassistant/components/airq/coordinator.py index 3ab41978b05..7c62a023a11 100644 --- a/homeassistant/components/airq/coordinator.py +++ b/homeassistant/components/airq/coordinator.py @@ -75,6 +75,7 @@ class AirQCoordinator(DataUpdateCoordinator): return_average=self.return_average, clip_negative_values=self.clip_negative, ) + data["brightness"] = await self.airq.get_current_brightness() if warming_up_sensors := identify_warming_up_sensors(data): _LOGGER.debug( "Following sensors are still warming up: %s", warming_up_sensors diff --git a/homeassistant/components/airq/number.py b/homeassistant/components/airq/number.py new file mode 100644 index 00000000000..e980760ed52 --- /dev/null +++ b/homeassistant/components/airq/number.py @@ -0,0 +1,85 @@ +"""Definition of air-Q number platform used to control the LED strips.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +import logging + +from aioairq.core import AirQ + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import AirQConfigEntry, AirQCoordinator + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class AirQBrightnessDescription(NumberEntityDescription): + """Describes AirQ number entity responsible for brightness control.""" + + value: Callable[[dict], float] + set_value: Callable[[AirQ, float], Awaitable[None]] + + +AIRQ_LED_BRIGHTNESS = AirQBrightnessDescription( + key="airq_led_brightness", + translation_key="airq_led_brightness", + native_min_value=0.0, + native_max_value=100.0, + native_step=1.0, + native_unit_of_measurement=PERCENTAGE, + value=lambda data: data["brightness"], + set_value=lambda device, value: device.set_current_brightness(value), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AirQConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up number entities: a single entity for the LEDs.""" + + coordinator = entry.runtime_data + entities = [AirQLEDBrightness(coordinator, AIRQ_LED_BRIGHTNESS)] + + async_add_entities(entities) + + +class AirQLEDBrightness(CoordinatorEntity[AirQCoordinator], NumberEntity): + """Representation of the LEDs from a single AirQ.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AirQCoordinator, + description: AirQBrightnessDescription, + ) -> None: + """Initialize a single sensor.""" + super().__init__(coordinator) + self.entity_description: AirQBrightnessDescription = description + + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.device_id}_{description.key}" + + @property + def native_value(self) -> float: + """Return the brightness of the LEDs in %.""" + return self.entity_description.value(self.coordinator.data) + + async def async_set_native_value(self, value: float) -> None: + """Set the brightness of the LEDs to the value in %.""" + _LOGGER.debug( + "Changing LED brighntess from %.0f%% to %.0f%%", + self.coordinator.data["brightness"], + value, + ) + await self.entity_description.set_value(self.coordinator.airq, value) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/airq/strings.json b/homeassistant/components/airq/strings.json index de8c7d86b09..2972ba5c15b 100644 --- a/homeassistant/components/airq/strings.json +++ b/homeassistant/components/airq/strings.json @@ -35,6 +35,11 @@ } }, "entity": { + "number": { + "airq_led_brightness": { + "name": "LED brightness" + } + }, "sensor": { "acetaldehyde": { "name": "Acetaldehyde" diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index 9df0e60850e..c08e2f1c010 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -1,11 +1,11 @@ """Alexa Devices integration.""" -from homeassistant.const import Platform +from homeassistant.const import CONF_COUNTRY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import _LOGGER, COUNTRY_DOMAINS, DOMAIN from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator from .services import async_setup_services @@ -40,6 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo return True +async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: + """Migrate old entry.""" + if entry.version == 1 and entry.minor_version == 0: + _LOGGER.debug( + "Migrating from version %s.%s", entry.version, entry.minor_version + ) + + # Convert country in domain + country = entry.data[CONF_COUNTRY] + domain = COUNTRY_DOMAINS.get(country, country) + + # Save domain and remove country + new_data = entry.data.copy() + new_data.update({"site": f"https://www.amazon.{domain}"}) + + hass.config_entries.async_update_entry( + entry, data=new_data, version=1, minor_version=1 + ) + + _LOGGER.info( + "Migration to version %s.%s successful", entry.version, entry.minor_version + ) + + return True + + async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index 3e705d73ade..d75ba39323d 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -10,16 +10,14 @@ from aioamazondevices.exceptions import ( CannotAuthenticate, CannotConnect, CannotRetrieveData, - WrongCountry, ) import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.selector import CountrySelector from .const import CONF_LOGIN_DATA, DOMAIN @@ -29,6 +27,12 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema( vol.Required(CONF_CODE): cv.string, } ) +STEP_RECONFIGURE = vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CODE): cv.string, + } +) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: @@ -37,7 +41,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, session = aiohttp_client.async_create_clientsession(hass) api = AmazonEchoApi( session, - data[CONF_COUNTRY], data[CONF_USERNAME], data[CONF_PASSWORD], ) @@ -48,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Alexa Devices.""" + VERSION = 1 + MINOR_VERSION = 1 + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -58,12 +64,10 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): data = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" - except CannotAuthenticate: + except (CannotAuthenticate, TypeError): errors["base"] = "invalid_auth" except CannotRetrieveData: errors["base"] = "cannot_retrieve_data" - except WrongCountry: - errors["base"] = "wrong_country" else: await self.async_set_unique_id(data["customer_info"]["user_id"]) self._abort_if_unique_id_configured() @@ -78,9 +82,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, data_schema=vol.Schema( { - vol.Required( - CONF_COUNTRY, default=self.hass.config.country - ): CountrySelector(), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_CODE): cv.string, @@ -109,7 +110,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): await validate_input(self.hass, {**reauth_entry.data, **user_input}) except CannotConnect: errors["base"] = "cannot_connect" - except CannotAuthenticate: + except (CannotAuthenticate, TypeError): errors["base"] = "invalid_auth" except CannotRetrieveData: errors["base"] = "cannot_retrieve_data" @@ -129,3 +130,47 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=STEP_REAUTH_DATA_SCHEMA, errors=errors, ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the device.""" + reconfigure_entry = self._get_reconfigure_entry() + if not user_input: + return self.async_show_form( + step_id="reconfigure", + data_schema=STEP_RECONFIGURE, + ) + + updated_password = user_input[CONF_PASSWORD] + + self._async_abort_entries_match( + {CONF_USERNAME: reconfigure_entry.data[CONF_USERNAME]} + ) + + errors: dict[str, str] = {} + + try: + data = await validate_input( + self.hass, {**reconfigure_entry.data, **user_input} + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except CannotAuthenticate: + errors["base"] = "invalid_auth" + except CannotRetrieveData: + errors["base"] = "cannot_retrieve_data" + else: + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates={ + CONF_PASSWORD: updated_password, + CONF_LOGIN_DATA: data, + }, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=STEP_RECONFIGURE, + errors=errors, + ) diff --git a/homeassistant/components/alexa_devices/const.py b/homeassistant/components/alexa_devices/const.py index ca0290a10bc..3ade3ad3ecd 100644 --- a/homeassistant/components/alexa_devices/const.py +++ b/homeassistant/components/alexa_devices/const.py @@ -6,3 +6,22 @@ _LOGGER = logging.getLogger(__package__) DOMAIN = "alexa_devices" CONF_LOGIN_DATA = "login_data" + +DEFAULT_DOMAIN = {"domain": "com"} +COUNTRY_DOMAINS = { + "ar": DEFAULT_DOMAIN, + "at": DEFAULT_DOMAIN, + "au": {"domain": "com.au"}, + "be": {"domain": "com.be"}, + "br": DEFAULT_DOMAIN, + "gb": {"domain": "co.uk"}, + "il": DEFAULT_DOMAIN, + "jp": {"domain": "co.jp"}, + "mx": {"domain": "com.mx"}, + "no": DEFAULT_DOMAIN, + "nz": {"domain": "com.au"}, + "pl": DEFAULT_DOMAIN, + "tr": {"domain": "com.tr"}, + "us": DEFAULT_DOMAIN, + "za": {"domain": "co.za"}, +} diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index f4a1faa4f81..7807c6f0efd 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -11,7 +11,7 @@ from aioamazondevices.exceptions import ( from aiohttp import ClientSession from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -44,7 +44,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): ) self.api = AmazonEchoApi( session, - entry.data[CONF_COUNTRY], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.data[CONF_LOGIN_DATA], @@ -67,7 +66,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): translation_key="cannot_retrieve_data_with_error", translation_placeholders={"error": repr(err)}, ) from err - except CannotAuthenticate as err: + except (CannotAuthenticate, TypeError) as err: raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth", diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 90410412dfa..cba3af83f44 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "silver", - "requirements": ["aioamazondevices==4.0.0"] + "requirements": ["aioamazondevices==5.0.0"] } diff --git a/homeassistant/components/alexa_devices/quality_scale.yaml b/homeassistant/components/alexa_devices/quality_scale.yaml index 5a2ff55b9b2..e2583b29e94 100644 --- a/homeassistant/components/alexa_devices/quality_scale.yaml +++ b/homeassistant/components/alexa_devices/quality_scale.yaml @@ -60,7 +60,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: no known use cases for repair issues or flows, yet diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index 1b1150d5649..b1e9027ca53 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -1,7 +1,6 @@ { "common": { "data_code": "One-time password (OTP code)", - "data_description_country": "The country where your Amazon account is registered.", "data_description_username": "The email address of your Amazon account.", "data_description_password": "The password of your Amazon account.", "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.", @@ -12,13 +11,11 @@ "step": { "user": { "data": { - "country": "[%key:common::config_flow::data::country%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "code": "[%key:component::alexa_devices::common::data_code%]" }, "data_description": { - "country": "[%key:component::alexa_devices::common::data_description_country%]", "username": "[%key:component::alexa_devices::common::data_description_username%]", "password": "[%key:component::alexa_devices::common::data_description_password%]", "code": "[%key:component::alexa_devices::common::data_description_code%]" @@ -33,6 +30,16 @@ "password": "[%key:component::alexa_devices::common::data_description_password%]", "code": "[%key:component::alexa_devices::common::data_description_code%]" } + }, + "reconfigure": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "code": "[%key:component::alexa_devices::common::data_code%]" + }, + "data_description": { + "password": "[%key:component::alexa_devices::common::data_description_password%]", + "code": "[%key:component::alexa_devices::common::data_description_code%]" + } } }, "abort": { @@ -40,13 +47,13 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.", "unknown": "[%key:common::config_flow::error::unknown%]" } }, diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index c25258e2e33..b5f034b4448 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, ) -from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN +from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT API_URL = "https://app.amber.com.au/developers" @@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): api = amberelectric.AmberApi(api_client) try: - sites: list[Site] = filter_sites(api.get_sites()) + sites: list[Site] = filter_sites( + api.get_sites(_request_timeout=REQUEST_TIMEOUT) + ) except amberelectric.ApiException as api_exception: if api_exception.status == 403: self._errors[CONF_API_TOKEN] = "invalid_api_token" diff --git a/homeassistant/components/amberelectric/const.py b/homeassistant/components/amberelectric/const.py index 490ef3dc2dc..3a1dbc9023a 100644 --- a/homeassistant/components/amberelectric/const.py +++ b/homeassistant/components/amberelectric/const.py @@ -21,3 +21,5 @@ SERVICE_GET_FORECASTS = "get_forecasts" GENERAL_CHANNEL = "general" CONTROLLED_LOAD_CHANNEL = "controlled_load" FEED_IN_CHANNEL = "feed_in" + +REQUEST_TIMEOUT = 15 diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index a1efef26aae..2ea14b5200b 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import LOGGER, REQUEST_TIMEOUT from .helpers import normalize_descriptor type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator] @@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): "grid": {}, } try: - data = self._api.get_current_prices(self.site_id, next=288) + data = self._api.get_current_prices( + self.site_id, + next=288, + _request_timeout=REQUEST_TIMEOUT, + ) intervals = [interval.actual_instance for interval in data] except ApiException as api_exception: raise UpdateFailed("Missing price data, skipping update") from api_exception diff --git a/homeassistant/components/ambient_station/entity.py b/homeassistant/components/ambient_station/entity.py index 24dfab438d8..9dec905b157 100644 --- a/homeassistant/components/ambient_station/entity.py +++ b/homeassistant/components/ambient_station/entity.py @@ -5,7 +5,7 @@ from __future__ import annotations from aioambient.util import get_public_device_id from homeassistant.core import callback -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, EntityDescription @@ -37,6 +37,7 @@ class AmbientWeatherEntity(Entity): identifiers={(DOMAIN, mac_address)}, manufacturer="Ambient Weather", name=station_name.capitalize(), + connections={(CONNECTION_NETWORK_MAC, mac_address)}, ) self._attr_unique_id = f"{mac_address}_{description.key}" diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 0d0f5183566..b1641e8dd48 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -390,7 +390,6 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]: async def async_devices_payload(hass: HomeAssistant) -> dict: """Return the devices payload.""" - integrations_without_model_id: set[str] = set() devices: list[dict[str, Any]] = [] dev_reg = dr.async_get(hass) # Devices that need via device info set @@ -400,10 +399,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: seen_integrations = set() for device in dev_reg.devices.values(): - # Ignore services - if device.entry_type: - continue - if not device.primary_config_entry: continue @@ -414,13 +409,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: seen_integrations.add(config_entry.domain) - if not device.model_id: - integrations_without_model_id.add(config_entry.domain) - continue - - if not device.manufacturer: - continue - new_indexes[device.id] = len(devices) devices.append( { @@ -432,8 +420,10 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: "hw_version": device.hw_version, "has_configuration_url": device.configuration_url is not None, "via_device": None, + "entry_type": device.entry_type.value if device.entry_type else None, } ) + if device.via_device_id: via_devices[device.id] = device.via_device_id @@ -453,15 +443,12 @@ async def async_devices_payload(hass: HomeAssistant) -> dict: for device_info in devices: if integration := integrations.get(device_info["integration"]): device_info["is_custom_integration"] = not integration.is_built_in + # Include version for custom integrations + if not integration.is_built_in and integration.version: + device_info["custom_integration_version"] = str(integration.version) return { "version": "home-assistant:1", - "no_model_id": sorted( - [ - domain - for domain in integrations_without_model_id - if domain in integrations and integrations[domain].is_built_in - ] - ), + "home_assistant": HA_VERSION, "devices": devices, } diff --git a/homeassistant/components/android_ip_webcam/coordinator.py b/homeassistant/components/android_ip_webcam/coordinator.py index c72d6ae1177..ec701cdf7d3 100644 --- a/homeassistant/components/android_ip_webcam/coordinator.py +++ b/homeassistant/components/android_ip_webcam/coordinator.py @@ -30,10 +30,9 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): cam: PyDroidIPCam, ) -> None: """Initialize the Android IP Webcam.""" - self.hass = hass self.cam = cam super().__init__( - self.hass, + hass, _LOGGER, config_entry=config_entry, name=f"{DOMAIN} {config_entry.data[CONF_HOST]}", diff --git a/homeassistant/components/anthropic/const.py b/homeassistant/components/anthropic/const.py index a1637a8cef6..356140ff66e 100644 --- a/homeassistant/components/anthropic/const.py +++ b/homeassistant/components/anthropic/const.py @@ -20,10 +20,8 @@ RECOMMENDED_THINKING_BUDGET = 0 MIN_THINKING_BUDGET = 1024 THINKING_MODELS = [ - "claude-3-7-sonnet-20250219", - "claude-3-7-sonnet-latest", - "claude-opus-4-20250514", - "claude-opus-4-0", - "claude-sonnet-4-20250514", + "claude-3-7-sonnet", "claude-sonnet-4-0", + "claude-opus-4-0", + "claude-opus-4-1", ] diff --git a/homeassistant/components/anthropic/entity.py b/homeassistant/components/anthropic/entity.py index 636417dd43b..7338cbe2906 100644 --- a/homeassistant/components/anthropic/entity.py +++ b/homeassistant/components/anthropic/entity.py @@ -2,11 +2,10 @@ from collections.abc import AsyncGenerator, Callable, Iterable import json -from typing import Any, cast +from typing import Any import anthropic from anthropic import AsyncStream -from anthropic._types import NOT_GIVEN from anthropic.types import ( InputJSONDelta, MessageDeltaUsage, @@ -17,7 +16,6 @@ from anthropic.types import ( RawContentBlockStopEvent, RawMessageDeltaEvent, RawMessageStartEvent, - RawMessageStopEvent, RedactedThinkingBlock, RedactedThinkingBlockParam, SignatureDelta, @@ -35,6 +33,7 @@ from anthropic.types import ( ToolUseBlockParam, Usage, ) +from anthropic.types.message_create_params import MessageCreateParamsStreaming from voluptuous_openapi import convert from homeassistant.components import conversation @@ -129,6 +128,28 @@ def _convert_content( ) ) + if isinstance(content.native, ThinkingBlock): + messages[-1]["content"].append( # type: ignore[union-attr] + ThinkingBlockParam( + type="thinking", + thinking=content.thinking_content or "", + signature=content.native.signature, + ) + ) + elif isinstance(content.native, RedactedThinkingBlock): + redacted_thinking_block = RedactedThinkingBlockParam( + type="redacted_thinking", + data=content.native.data, + ) + if isinstance(messages[-1]["content"], str): + messages[-1]["content"] = [ + TextBlockParam(type="text", text=messages[-1]["content"]), + redacted_thinking_block, + ] + else: + messages[-1]["content"].append( # type: ignore[attr-defined] + redacted_thinking_block + ) if content.content: messages[-1]["content"].append( # type: ignore[union-attr] TextBlockParam(type="text", text=content.content) @@ -152,10 +173,9 @@ def _convert_content( return messages -async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place +async def _transform_stream( chat_log: conversation.ChatLog, - result: AsyncStream[MessageStreamEvent], - messages: list[MessageParam], + stream: AsyncStream[MessageStreamEvent], ) -> AsyncGenerator[conversation.AssistantContentDeltaDict]: """Transform the response stream into HA format. @@ -186,31 +206,25 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have Each message could contain multiple blocks of the same type. """ - if result is None: + if stream is None: raise TypeError("Expected a stream of messages") - current_message: MessageParam | None = None - current_block: ( - TextBlockParam - | ToolUseBlockParam - | ThinkingBlockParam - | RedactedThinkingBlockParam - | None - ) = None + current_tool_block: ToolUseBlockParam | None = None current_tool_args: str input_usage: Usage | None = None + has_content = False + has_native = False - async for response in result: + async for response in stream: LOGGER.debug("Received response: %s", response) if isinstance(response, RawMessageStartEvent): if response.message.role != "assistant": raise ValueError("Unexpected message role") - current_message = MessageParam(role=response.message.role, content=[]) input_usage = response.message.usage elif isinstance(response, RawContentBlockStartEvent): if isinstance(response.content_block, ToolUseBlock): - current_block = ToolUseBlockParam( + current_tool_block = ToolUseBlockParam( type="tool_use", id=response.content_block.id, name=response.content_block.name, @@ -218,75 +232,64 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have ) current_tool_args = "" elif isinstance(response.content_block, TextBlock): - current_block = TextBlockParam( - type="text", text=response.content_block.text - ) - yield {"role": "assistant"} + if has_content: + yield {"role": "assistant"} + has_native = False + has_content = True if response.content_block.text: yield {"content": response.content_block.text} elif isinstance(response.content_block, ThinkingBlock): - current_block = ThinkingBlockParam( - type="thinking", - thinking=response.content_block.thinking, - signature=response.content_block.signature, - ) + if has_native: + yield {"role": "assistant"} + has_native = False + has_content = False elif isinstance(response.content_block, RedactedThinkingBlock): - current_block = RedactedThinkingBlockParam( - type="redacted_thinking", data=response.content_block.data - ) LOGGER.debug( "Some of Claude’s internal reasoning has been automatically " "encrypted for safety reasons. This doesn’t affect the quality of " "responses" ) + if has_native: + yield {"role": "assistant"} + has_native = False + has_content = False + yield {"native": response.content_block} + has_native = True elif isinstance(response, RawContentBlockDeltaEvent): - if current_block is None: - raise ValueError("Unexpected delta without a block") if isinstance(response.delta, InputJSONDelta): current_tool_args += response.delta.partial_json elif isinstance(response.delta, TextDelta): - text_block = cast(TextBlockParam, current_block) - text_block["text"] += response.delta.text yield {"content": response.delta.text} elif isinstance(response.delta, ThinkingDelta): - thinking_block = cast(ThinkingBlockParam, current_block) - thinking_block["thinking"] += response.delta.thinking + yield {"thinking_content": response.delta.thinking} elif isinstance(response.delta, SignatureDelta): - thinking_block = cast(ThinkingBlockParam, current_block) - thinking_block["signature"] += response.delta.signature + yield { + "native": ThinkingBlock( + type="thinking", + thinking="", + signature=response.delta.signature, + ) + } + has_native = True elif isinstance(response, RawContentBlockStopEvent): - if current_block is None: - raise ValueError("Unexpected stop event without a current block") - if current_block["type"] == "tool_use": - # tool block + if current_tool_block is not None: tool_args = json.loads(current_tool_args) if current_tool_args else {} - current_block["input"] = tool_args + current_tool_block["input"] = tool_args yield { "tool_calls": [ llm.ToolInput( - id=current_block["id"], - tool_name=current_block["name"], + id=current_tool_block["id"], + tool_name=current_tool_block["name"], tool_args=tool_args, ) ] } - elif current_block["type"] == "thinking": - # thinking block - LOGGER.debug("Thinking: %s", current_block["thinking"]) - - if current_message is None: - raise ValueError("Unexpected stop event without a current message") - current_message["content"].append(current_block) # type: ignore[union-attr] - current_block = None + current_tool_block = None elif isinstance(response, RawMessageDeltaEvent): if (usage := response.usage) is not None: chat_log.async_trace(_create_token_stats(input_usage, usage)) if response.delta.stop_reason == "refusal": raise HomeAssistantError("Potential policy violation detected") - elif isinstance(response, RawMessageStopEvent): - if current_message is not None: - messages.append(current_message) - current_message = None def _create_token_stats( @@ -351,45 +354,48 @@ class AnthropicBaseLLMEntity(Entity): thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET) model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL) + model_args = MessageCreateParamsStreaming( + model=model, + messages=messages, + max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS), + system=system.content, + stream=True, + ) + if tools: + model_args["tools"] = tools + if ( + model.startswith(tuple(THINKING_MODELS)) + and thinking_budget >= MIN_THINKING_BUDGET + ): + model_args["thinking"] = ThinkingConfigEnabledParam( + type="enabled", budget_tokens=thinking_budget + ) + else: + model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled") + model_args["temperature"] = options.get( + CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE + ) + # To prevent infinite loops, we limit the number of iterations for _iteration in range(MAX_TOOL_ITERATIONS): - model_args = { - "model": model, - "messages": messages, - "tools": tools or NOT_GIVEN, - "max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS), - "system": system.content, - "stream": True, - } - if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET: - model_args["thinking"] = ThinkingConfigEnabledParam( - type="enabled", budget_tokens=thinking_budget - ) - else: - model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled") - model_args["temperature"] = options.get( - CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE - ) - try: stream = await client.messages.create(**model_args) + + messages.extend( + _convert_content( + [ + content + async for content in chat_log.async_add_delta_content_stream( + self.entity_id, + _transform_stream(chat_log, stream), + ) + ] + ) + ) except anthropic.AnthropicError as err: raise HomeAssistantError( f"Sorry, I had a problem talking to Anthropic: {err}" ) from err - messages.extend( - _convert_content( - [ - content - async for content in chat_log.async_add_delta_content_stream( - self.entity_id, - _transform_stream(chat_log, stream, messages), - ) - if not isinstance(content, conversation.AssistantContent) - ] - ) - ) - if not chat_log.unresponded_tool_results: break diff --git a/homeassistant/components/anthropic/manifest.json b/homeassistant/components/anthropic/manifest.json index 6a8f1e5e54c..6fed0282a00 100644 --- a/homeassistant/components/anthropic/manifest.json +++ b/homeassistant/components/anthropic/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/anthropic", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["anthropic==0.52.0"] + "requirements": ["anthropic==0.62.0"] } diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index dfeb56c8d06..394ff4c4088 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -10,9 +10,9 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator +from .entity import APCUPSdEntity PARALLEL_UPDATES = 0 @@ -40,22 +40,16 @@ async def async_setup_entry( async_add_entities([OnlineStatus(coordinator, _DESCRIPTION)]) -class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity): +class OnlineStatus(APCUPSdEntity, BinarySensorEntity): """Representation of a UPS online status.""" - _attr_has_entity_name = True - def __init__( self, coordinator: APCUPSdCoordinator, description: BinarySensorEntityDescription, ) -> None: """Initialize the APCUPSd binary device.""" - super().__init__(coordinator, context=description.key.upper()) - - self.entity_description = description - self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}" - self._attr_device_info = coordinator.device_info + super().__init__(coordinator, description) @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/apcupsd/entity.py b/homeassistant/components/apcupsd/entity.py new file mode 100644 index 00000000000..9ebe51ff876 --- /dev/null +++ b/homeassistant/components/apcupsd/entity.py @@ -0,0 +1,26 @@ +"""Base entity for APCUPSd integration.""" + +from __future__ import annotations + +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .coordinator import APCUPSdCoordinator + + +class APCUPSdEntity(CoordinatorEntity[APCUPSdCoordinator]): + """Base entity for APCUPSd integration.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: APCUPSdCoordinator, + description: EntityDescription, + ) -> None: + """Initialize the APCUPSd entity.""" + super().__init__(coordinator, context=description.key.upper()) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}" + self._attr_device_info = coordinator.device_info diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 3713b74fff7..5e5a81c358a 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/apcupsd", "iot_class": "local_polling", "loggers": ["apcaccess"], + "quality_scale": "bronze", "requirements": ["aioapcaccess==0.4.2"] } diff --git a/homeassistant/components/apcupsd/quality_scale.yaml b/homeassistant/components/apcupsd/quality_scale.yaml new file mode 100644 index 00000000000..23b72134d34 --- /dev/null +++ b/homeassistant/components/apcupsd/quality_scale.yaml @@ -0,0 +1,90 @@ +rules: + # Bronze + action-setup: done + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: | + The integration does not provide any actions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + Entities of this integration does not explicitly subscribe to events. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + # Silver + action-exceptions: + status: exempt + comment: | + The integration does not provide any actions. + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: | + The integration does not provide any additional options. + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: + status: exempt + comment: | + The integration does not require authentication. + test-coverage: + status: todo + comment: | + Patch `aioapcaccess.request_status` where we use it. + # Gold + devices: done + diagnostics: done + discovery-update-info: + status: exempt + comment: | + This integration cannot be discovered. + discovery: + status: exempt + comment: | + This integration cannot be discovered. + docs-data-update: done + docs-examples: done + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done + dynamic-devices: + status: exempt + comment: | + The integration connects to a single service per configuration entry. + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: done + icon-translations: done + reconfiguration-flow: done + repair-issues: done + stale-devices: + status: exempt + comment: | + This integration connect to a single service per configuration entry. + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: | + The integration does not connect via HTTP. + strict-typing: done diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 5076b537467..14baed5bfce 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -23,10 +23,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import LAST_S_TEST from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator +from .entity import APCUPSdEntity PARALLEL_UPDATES = 0 @@ -490,22 +490,16 @@ def infer_unit(value: str) -> tuple[str, str | None]: return value, None -class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity): +class APCUPSdSensor(APCUPSdEntity, SensorEntity): """Representation of a sensor entity for APCUPSd status values.""" - _attr_has_entity_name = True - def __init__( self, coordinator: APCUPSdCoordinator, description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator=coordinator, context=description.key.upper()) - - self.entity_description = description - self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}" - self._attr_device_info = coordinator.device_info + super().__init__(coordinator, description) # Initial update of attributes. self._update_attrs() diff --git a/homeassistant/components/apcupsd/strings.json b/homeassistant/components/apcupsd/strings.json index d821b66ef67..8f237fd41fe 100644 --- a/homeassistant/components/apcupsd/strings.json +++ b/homeassistant/components/apcupsd/strings.json @@ -14,7 +14,22 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" }, + "data_description": { + "host": "The hostname or IP address of the APC UPS Daemon", + "port": "The port the APC UPS Daemon is listening on" + }, "description": "Enter the host and port on which the apcupsd NIS is being served." + }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "[%key:component::apcupsd::config::step::user::data_description::host%]", + "port": "[%key:component::apcupsd::config::step::user::data_description::port%]" + }, + "description": "[%key:component::apcupsd::config::step::user::description%]" } } }, diff --git a/homeassistant/components/assist_satellite/entity.py b/homeassistant/components/assist_satellite/entity.py index e7a10ef63f6..3d562544c68 100644 --- a/homeassistant/components/assist_satellite/entity.py +++ b/homeassistant/components/assist_satellite/entity.py @@ -11,7 +11,7 @@ import time from typing import Any, Literal, final from hassil import Intents, recognize -from hassil.expression import Expression, ListReference, Sequence +from hassil.expression import Expression, Group, ListReference from hassil.intents import WildcardSlotList from homeassistant.components import conversation, media_source, stt, tts @@ -413,7 +413,7 @@ class AssistSatelliteEntity(entity.Entity): for intent in intents.intents.values(): for intent_data in intent.data: for sentence in intent_data.sentences: - _collect_list_references(sentence, wildcard_names) + _collect_list_references(sentence.expression, wildcard_names) for wildcard_name in wildcard_names: intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name) @@ -727,9 +727,9 @@ class AssistSatelliteEntity(entity.Entity): def _collect_list_references(expression: Expression, list_names: set[str]) -> None: """Collect list reference names recursively.""" - if isinstance(expression, Sequence): - seq: Sequence = expression - for item in seq.items: + if isinstance(expression, Group): + grp: Group = expression + for item in grp.items: _collect_list_references(item, list_names) elif isinstance(expression, ListReference): # {list} diff --git a/homeassistant/components/assist_satellite/manifest.json b/homeassistant/components/assist_satellite/manifest.json index 97362f157e4..184de576050 100644 --- a/homeassistant/components/assist_satellite/manifest.json +++ b/homeassistant/components/assist_satellite/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/assist_satellite", "integration_type": "entity", "quality_scale": "internal", - "requirements": ["hassil==2.2.3"] + "requirements": ["hassil==3.1.0"] } diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index bc6f0fe6fd2..6e33f3a0b43 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -5,15 +5,17 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections import namedtuple from collections.abc import Awaitable, Callable, Coroutine -from datetime import datetime import functools import logging from typing import Any, cast from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy from aiohttp import ClientSession -from pyasuswrt import AsusWrtError, AsusWrtHttp -from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError +from asusrouter import AsusRouter, AsusRouterError +from asusrouter.modules.client import AsusClient +from asusrouter.modules.data import AsusData +from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors +from asusrouter.tools.connection import get_cookie_jar from homeassistant.const import ( CONF_HOST, @@ -24,7 +26,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import UpdateFailed @@ -41,14 +43,13 @@ from .const import ( PROTOCOL_HTTPS, PROTOCOL_TELNET, SENSORS_BYTES, - SENSORS_CPU, SENSORS_LOAD_AVG, SENSORS_MEMORY, SENSORS_RATES, - SENSORS_TEMPERATURES, SENSORS_TEMPERATURES_LEGACY, SENSORS_UPTIME, ) +from .helpers import clean_dict, translate_to_legacy SENSORS_TYPE_BYTES = "sensors_bytes" SENSORS_TYPE_COUNT = "sensors_count" @@ -109,7 +110,10 @@ class AsusWrtBridge(ABC): ) -> AsusWrtBridge: """Get Bridge instance.""" if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP): - session = async_get_clientsession(hass) + session = async_create_clientsession( + hass, + cookie_jar=get_cookie_jar(), + ) return AsusWrtHttpBridge(conf, session) return AsusWrtLegacyBridge(conf, options) @@ -310,16 +314,16 @@ class AsusWrtHttpBridge(AsusWrtBridge): def __init__(self, conf: dict[str, Any], session: ClientSession) -> None: """Initialize Bridge that use HTTP library.""" super().__init__(conf[CONF_HOST]) - self._api: AsusWrtHttp = self._get_api(conf, session) + self._api = self._get_api(conf, session) @staticmethod - def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusWrtHttp: - """Get the AsusWrtHttp API.""" - return AsusWrtHttp( - conf[CONF_HOST], - conf[CONF_USERNAME], - conf.get(CONF_PASSWORD, ""), - use_https=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS, + def _get_api(conf: dict[str, Any], session: ClientSession) -> AsusRouter: + """Get the AsusRouter API.""" + return AsusRouter( + hostname=conf[CONF_HOST], + username=conf[CONF_USERNAME], + password=conf.get(CONF_PASSWORD, ""), + use_ssl=conf[CONF_PROTOCOL] == PROTOCOL_HTTPS, port=conf.get(CONF_PORT), session=session, ) @@ -327,46 +331,90 @@ class AsusWrtHttpBridge(AsusWrtBridge): @property def is_connected(self) -> bool: """Get connected status.""" - return cast(bool, self._api.is_connected) + return self._api.connected async def async_connect(self) -> None: """Connect to the device.""" await self._api.async_connect() + # Collect the identity + _identity = await self._api.async_get_identity() + # get main router properties - if mac := self._api.mac: + if mac := _identity.mac: self._label_mac = format_mac(mac) - self._firmware = self._api.firmware - self._model = self._api.model + self._firmware = str(_identity.firmware) + self._model = _identity.model async def async_disconnect(self) -> None: """Disconnect to the device.""" await self._api.async_disconnect() + async def _get_data( + self, + datatype: AsusData, + force: bool = False, + ) -> dict[str, Any]: + """Get data from the device. + + This is a generic method which automatically converts to + the Home Assistant-compatible format. + """ + try: + raw = await self._api.async_get_data(datatype, force=force) + return translate_to_legacy(clean_dict(convert_to_ha_data(raw))) + except AsusRouterError as ex: + raise UpdateFailed(ex) from ex + + async def _get_sensors(self, datatype: AsusData) -> list[str]: + """Get the available sensors. + + This is a generic method which automatically converts to + the Home Assistant-compatible format. + """ + sensors = [] + try: + data = await self._api.async_get_data(datatype) + # Get the list of sensors from the raw data + # and translate in to the legacy format + sensors = translate_to_legacy(convert_to_ha_sensors(data, datatype)) + _LOGGER.debug("Available `%s` sensors: %s", datatype.value, sensors) + except AsusRouterError as ex: + _LOGGER.warning( + "Cannot get available `%s` sensors with exception: %s", + datatype.value, + ex, + ) + return sensors + async def async_get_connected_devices(self) -> dict[str, WrtDevice]: """Get list of connected devices.""" - api_devices = await self._api.async_get_connected_devices() + api_devices: dict[str, AsusClient] = await self._api.async_get_data( + AsusData.CLIENTS, force=True + ) return { - format_mac(mac): WrtDevice(dev.ip, dev.name, dev.node) + format_mac(mac): WrtDevice( + dev.connection.ip_address, dev.description.name, dev.connection.node + ) for mac, dev in api_devices.items() + if dev.connection is not None + and dev.description is not None + and dev.connection.ip_address is not None } async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]: """Return a dictionary of available sensors for this bridge.""" - sensors_cpu = await self._get_available_cpu_sensors() - sensors_temperatures = await self._get_available_temperature_sensors() - sensors_loadavg = await self._get_loadavg_sensors_availability() return { SENSORS_TYPE_BYTES: { KEY_SENSORS: SENSORS_BYTES, KEY_METHOD: self._get_bytes, }, SENSORS_TYPE_CPU: { - KEY_SENSORS: sensors_cpu, + KEY_SENSORS: await self._get_sensors(AsusData.CPU), KEY_METHOD: self._get_cpu_usage, }, SENSORS_TYPE_LOAD_AVG: { - KEY_SENSORS: sensors_loadavg, + KEY_SENSORS: await self._get_sensors(AsusData.SYSINFO), KEY_METHOD: self._get_load_avg, }, SENSORS_TYPE_MEMORY: { @@ -382,95 +430,44 @@ class AsusWrtHttpBridge(AsusWrtBridge): KEY_METHOD: self._get_uptime, }, SENSORS_TYPE_TEMPERATURES: { - KEY_SENSORS: sensors_temperatures, + KEY_SENSORS: await self._get_sensors(AsusData.TEMPERATURE), KEY_METHOD: self._get_temperatures, }, } - async def _get_available_cpu_sensors(self) -> list[str]: - """Check which cpu information is available on the router.""" - try: - available_cpu = await self._api.async_get_cpu_usage() - available_sensors = [t for t in SENSORS_CPU if t in available_cpu] - except AsusWrtError as exc: - _LOGGER.warning( - ( - "Failed checking cpu sensor availability for ASUS router" - " %s. Exception: %s" - ), - self.host, - exc, - ) - return [] - return available_sensors - - async def _get_available_temperature_sensors(self) -> list[str]: - """Check which temperature information is available on the router.""" - try: - available_temps = await self._api.async_get_temperatures() - available_sensors = [ - t for t in SENSORS_TEMPERATURES if t in available_temps - ] - except AsusWrtError as exc: - _LOGGER.warning( - ( - "Failed checking temperature sensor availability for ASUS router" - " %s. Exception: %s" - ), - self.host, - exc, - ) - return [] - return available_sensors - - async def _get_loadavg_sensors_availability(self) -> list[str]: - """Check if load avg is available on the router.""" - try: - await self._api.async_get_loadavg() - except AsusWrtNotAvailableInfoError: - return [] - except AsusWrtError: - pass - return SENSORS_LOAD_AVG - - @handle_errors_and_zip(AsusWrtError, SENSORS_BYTES) async def _get_bytes(self) -> Any: """Fetch byte information from the router.""" - return await self._api.async_get_traffic_bytes() + return await self._get_data(AsusData.NETWORK) - @handle_errors_and_zip(AsusWrtError, SENSORS_RATES) async def _get_rates(self) -> Any: """Fetch rates information from the router.""" - return await self._api.async_get_traffic_rates() + data = await self._get_data(AsusData.NETWORK) + # Convert from bits/s to Bytes/s for compatibility with legacy sensors + return { + key: ( + value / 8 + if key in SENSORS_RATES and isinstance(value, (int, float)) + else value + ) + for key, value in data.items() + } - @handle_errors_and_zip(AsusWrtError, SENSORS_LOAD_AVG) async def _get_load_avg(self) -> Any: """Fetch cpu load avg information from the router.""" - return await self._api.async_get_loadavg() + return await self._get_data(AsusData.SYSINFO) - @handle_errors_and_zip(AsusWrtError, None) async def _get_temperatures(self) -> Any: """Fetch temperatures information from the router.""" - return await self._api.async_get_temperatures() + return await self._get_data(AsusData.TEMPERATURE) - @handle_errors_and_zip(AsusWrtError, None) async def _get_cpu_usage(self) -> Any: """Fetch cpu information from the router.""" - return await self._api.async_get_cpu_usage() + return await self._get_data(AsusData.CPU) - @handle_errors_and_zip(AsusWrtError, None) async def _get_memory_usage(self) -> Any: """Fetch memory information from the router.""" - return await self._api.async_get_memory_usage() + return await self._get_data(AsusData.RAM) async def _get_uptime(self) -> dict[str, Any]: """Fetch uptime from the router.""" - try: - uptimes = await self._api.async_get_uptime() - except AsusWrtError as exc: - raise UpdateFailed(exc) from exc - - last_boot = datetime.fromisoformat(uptimes["last_boot"]) - uptime = uptimes["uptime"] - - return dict(zip(SENSORS_UPTIME, [last_boot, uptime], strict=False)) + return await self._get_data(AsusData.BOOTTIME) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index d58a216aaee..a86f7e08318 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -7,7 +7,7 @@ import os import socket from typing import Any, cast -from pyasuswrt import AsusWrtError +from asusrouter import AsusRouterError import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -189,7 +189,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): try: await api.async_connect() - except (AsusWrtError, OSError): + except (AsusRouterError, OSError): _LOGGER.error( "Error connecting to the AsusWrt router at %s using protocol %s", host, diff --git a/homeassistant/components/asuswrt/helpers.py b/homeassistant/components/asuswrt/helpers.py new file mode 100644 index 00000000000..0fb467e6046 --- /dev/null +++ b/homeassistant/components/asuswrt/helpers.py @@ -0,0 +1,56 @@ +"""Helpers for AsusWRT integration.""" + +from __future__ import annotations + +from typing import Any, TypeVar + +T = TypeVar("T", dict[str, Any], list[Any], None) + +TRANSLATION_MAP = { + "wan_rx": "sensor_rx_bytes", + "wan_tx": "sensor_tx_bytes", + "total_usage": "cpu_total_usage", + "usage": "mem_usage_perc", + "free": "mem_free", + "used": "mem_used", + "wan_rx_speed": "sensor_rx_rates", + "wan_tx_speed": "sensor_tx_rates", + "2ghz": "2.4GHz", + "5ghz": "5.0GHz", + "5ghz2": "5.0GHz_2", + "6ghz": "6.0GHz", + "cpu": "CPU", + "datetime": "sensor_last_boot", + "uptime": "sensor_uptime", + **{f"{num}_usage": f"cpu{num}_usage" for num in range(1, 9)}, + **{f"load_avg_{load}": f"sensor_load_avg{load}" for load in ("1", "5", "15")}, +} + + +def clean_dict(raw: dict[str, Any]) -> dict[str, Any]: + """Cleans dictionary from None values. + + The `state` key is always preserved regardless of its value. + """ + + return {k: v for k, v in raw.items() if v is not None or k.endswith("state")} + + +def translate_to_legacy(raw: T) -> T: + """Translate raw data to legacy format for dicts and lists.""" + + if raw is None: + return None + + if isinstance(raw, dict): + return {TRANSLATION_MAP.get(k, k): v for k, v in raw.items()} + + if isinstance(raw, list): + return [ + TRANSLATION_MAP[item] + if isinstance(item, str) and item in TRANSLATION_MAP + else item + for item in raw + ] + + return raw diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index f4b2e3386e9..c5bdb9440f5 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -1,11 +1,11 @@ { "domain": "asuswrt", "name": "ASUSWRT", - "codeowners": ["@kennedyshead", "@ollo69"], + "codeowners": ["@kennedyshead", "@ollo69", "@Vaskivskyi"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/asuswrt", "integration_type": "hub", "iot_class": "local_polling", - "loggers": ["aioasuswrt", "asyncssh"], - "requirements": ["aioasuswrt==1.4.0", "pyasuswrt==0.1.21"] + "loggers": ["aioasuswrt", "asusrouter", "asyncssh"], + "requirements": ["aioasuswrt==1.4.0", "asusrouter==1.20.0"] } diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 3cf8d2e863d..c777535e242 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta import logging from typing import TYPE_CHECKING, Any -from pyasuswrt import AsusWrtError +from asusrouter import AsusRouterError from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, @@ -229,7 +229,7 @@ class AsusWrtRouter: """Set up a AsusWrt router.""" try: await self._api.async_connect() - except (AsusWrtError, OSError) as exc: + except (AsusRouterError, OSError) as exc: raise ConfigEntryNotReady from exc if not self._api.is_connected: raise ConfigEntryNotReady @@ -284,7 +284,7 @@ class AsusWrtRouter: _LOGGER.debug("Checking devices for ASUS router %s", self.host) try: wrt_devices = await self._api.async_get_connected_devices() - except (OSError, AsusWrtError) as exc: + except (OSError, AsusRouterError) as exc: if not self._connect_error: self._connect_error = True _LOGGER.error( diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 434db46384b..38a3ade2d90 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -6,18 +6,21 @@ from pathlib import Path from typing import cast from aiohttp import ClientResponseError -from yalexs.const import Brand from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from yalexs.manager.gateway import Config as YaleXSConfig from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, issue_registry as ir +from homeassistant.helpers import ( + config_entry_oauth2_flow, + device_registry as dr, + issue_registry as ir, +) -from .const import DOMAIN, PLATFORMS +from .const import DEFAULT_AUGUST_BRAND, DOMAIN, PLATFORMS from .data import AugustData from .gateway import AugustGateway from .util import async_create_august_clientsession @@ -25,30 +28,21 @@ from .util import async_create_august_clientsession type AugustConfigEntry = ConfigEntry[AugustData] -@callback -def _async_create_yale_brand_migration_issue( - hass: HomeAssistant, entry: AugustConfigEntry -) -> None: - """Create an issue for a brand migration.""" - ir.async_create_issue( - hass, - DOMAIN, - "yale_brand_migration", - breaks_in_ha_version="2024.9", - learn_more_url="https://www.home-assistant.io/integrations/yale", - translation_key="yale_brand_migration", - is_fixable=False, - severity=ir.IssueSeverity.CRITICAL, - translation_placeholders={ - "migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale" - }, - ) - - async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool: """Set up August from a config entry.""" + # Check if this is a legacy config entry that needs migration to OAuth + if "auth_implementation" not in entry.data: + # This is a legacy entry using username/password, trigger reauth + raise ConfigEntryAuthFailed("Migration to OAuth required") + session = async_create_august_clientsession(hass) - august_gateway = AugustGateway(Path(hass.config.config_dir), session) + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session) try: await async_setup_august(hass, entry, august_gateway) except (RequireValidation, InvalidAuth) as err: @@ -76,9 +70,7 @@ async def async_setup_august( ) -> None: """Set up the August component.""" config = cast(YaleXSConfig, entry.data) - await august_gateway.async_setup(config) - if august_gateway.api.brand == Brand.YALE_HOME: - _async_create_yale_brand_migration_issue(hass, entry) + await august_gateway.async_setup({**config, "brand": DEFAULT_AUGUST_BRAND}) await august_gateway.async_authenticate() await august_gateway.async_refresh_access_token_if_needed() data = entry.runtime_data = AugustData(hass, august_gateway) diff --git a/homeassistant/components/august/application_credentials.py b/homeassistant/components/august/application_credentials.py new file mode 100644 index 00000000000..ce63014aec5 --- /dev/null +++ b/homeassistant/components/august/application_credentials.py @@ -0,0 +1,15 @@ +"""application_credentials platform for the august integration.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +OAUTH2_AUTHORIZE = "https://auth.august.com/authorization" +OAUTH2_TOKEN = "https://auth.august.com/access_token" + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 640b04b384f..4fc7884ce00 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -1,284 +1,86 @@ """Config flow for August integration.""" from collections.abc import Mapping -from dataclasses import dataclass import logging -from pathlib import Path from typing import Any -import aiohttp -import voluptuous as vol -from yalexs.authenticator_common import ValidationResult -from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND, Brand -from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation +import jwt -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback - -from .const import ( - CONF_ACCESS_TOKEN_CACHE_FILE, - CONF_BRAND, - CONF_LOGIN_METHOD, - DEFAULT_LOGIN_METHOD, - DOMAIN, - LOGIN_METHODS, - VERIFICATION_CODE_KEY, -) -from .gateway import AugustGateway -from .util import async_create_august_clientsession - -# The Yale Home Brand is not supported by the August integration -# anymore and should migrate to the Yale integration -AVAILABLE_BRANDS = BRANDS_WITHOUT_OAUTH.copy() -del AVAILABLE_BRANDS[Brand.YALE_HOME] +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult +from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_validate_input( - data: dict[str, Any], august_gateway: AugustGateway -) -> dict[str, Any]: - """Validate the user input allows us to connect. - - Data has the keys from DATA_SCHEMA with values provided by the user. - - Request configuration steps from the user. - """ - assert august_gateway.authenticator is not None - authenticator = august_gateway.authenticator - if (code := data.get(VERIFICATION_CODE_KEY)) is not None: - result = await authenticator.async_validate_verification_code(code) - _LOGGER.debug("Verification code validation: %s", result) - if result != ValidationResult.VALIDATED: - raise RequireValidation - - try: - await august_gateway.async_authenticate() - except RequireValidation: - _LOGGER.debug( - "Requesting new verification code for %s via %s", - data.get(CONF_USERNAME), - data.get(CONF_LOGIN_METHOD), - ) - if code is None: - await august_gateway.authenticator.async_send_verification_code() - raise - - return { - "title": data.get(CONF_USERNAME), - "data": august_gateway.config_entry(), - } - - -@dataclass(slots=True) -class ValidateResult: - """Result from validation.""" - - validation_required: bool - info: dict[str, Any] - errors: dict[str, str] - description_placeholders: dict[str, str] - - -class AugustConfigFlow(ConfigFlow, domain=DOMAIN): +class AugustConfigFlow( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): """Handle a config flow for August.""" VERSION = 1 + DOMAIN = DOMAIN - def __init__(self) -> None: - """Store an AugustGateway().""" - self._august_gateway: AugustGateway | None = None - self._aiohttp_session: aiohttp.ClientSession | None = None - self._user_auth_details: dict[str, Any] = {} - self._needs_reset = True - super().__init__() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the initial step.""" - return await self.async_step_user_validate() - - async def async_step_user_validate( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle authentication.""" - errors: dict[str, str] = {} - description_placeholders: dict[str, str] = {} - if user_input is not None: - self._user_auth_details.update(user_input) - validate_result = await self._async_auth_or_validate() - description_placeholders = validate_result.description_placeholders - if validate_result.validation_required: - return await self.async_step_validation() - if not (errors := validate_result.errors): - return await self._async_update_or_create_entry(validate_result.info) - - return self.async_show_form( - step_id="user_validate", - data_schema=vol.Schema( - { - vol.Required( - CONF_BRAND, - default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), - ): vol.In(AVAILABLE_BRANDS), - vol.Required( - CONF_LOGIN_METHOD, - default=self._user_auth_details.get( - CONF_LOGIN_METHOD, DEFAULT_LOGIN_METHOD - ), - ): vol.In(LOGIN_METHODS), - vol.Required( - CONF_USERNAME, - default=self._user_auth_details.get(CONF_USERNAME), - ): str, - vol.Required(CONF_PASSWORD): str, - } - ), - errors=errors, - description_placeholders=description_placeholders, - ) - - async def async_step_validation( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle validation (2fa) step.""" - if user_input: - if self.source == SOURCE_REAUTH: - return await self.async_step_reauth_validate(user_input) - return await self.async_step_user_validate(user_input) - - previously_failed = VERIFICATION_CODE_KEY in self._user_auth_details - return self.async_show_form( - step_id="validation", - data_schema=vol.Schema( - {vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)} - ), - errors={"base": "invalid_verification_code"} if previously_failed else None, - description_placeholders={ - CONF_BRAND: self._user_auth_details[CONF_BRAND], - CONF_USERNAME: self._user_auth_details[CONF_USERNAME], - CONF_LOGIN_METHOD: self._user_auth_details[CONF_LOGIN_METHOD], - }, - ) - - @callback - def _async_get_gateway(self) -> AugustGateway: - """Set up the gateway.""" - if self._august_gateway is not None: - return self._august_gateway - self._aiohttp_session = async_create_august_clientsession(self.hass) - self._august_gateway = AugustGateway( - Path(self.hass.config.config_dir), self._aiohttp_session - ) - return self._august_gateway - - @callback - def _async_shutdown_gateway(self) -> None: - """Shutdown the gateway.""" - if self._aiohttp_session is not None: - self._aiohttp_session.detach() - self._august_gateway = None + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return _LOGGER async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Handle configuration by re-auth.""" - self._user_auth_details = dict(entry_data) - return await self.async_step_reauth_validate() + return await self.async_step_user() - async def async_step_reauth_validate( - self, user_input: dict[str, Any] | None = None + def _async_decode_jwt(self, encoded: str) -> dict[str, Any]: + """Decode JWT token.""" + return jwt.decode( + encoded, + "", + verify=False, + options={"verify_signature": False}, + algorithms=["HS256"], + ) + + async def _async_handle_reauth( + self, data: dict, decoded: dict[str, Any], user_id: str ) -> ConfigFlowResult: - """Handle reauth and validation.""" - errors: dict[str, str] = {} - description_placeholders: dict[str, str] = {} - if user_input is not None: - self._user_auth_details.update(user_input) - validate_result = await self._async_auth_or_validate() - description_placeholders = validate_result.description_placeholders - if validate_result.validation_required: - return await self.async_step_validation() - if not (errors := validate_result.errors): - return await self._async_update_or_create_entry(validate_result.info) + """Handle reauth flow.""" + reauth_entry = self._get_reauth_entry() + assert reauth_entry.unique_id is not None + # Check if this is a migration from username (contains @) to user ID + if "@" not in reauth_entry.unique_id: + # This is a normal oauth reauth, enforce ID matching for security + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_mismatch(reason="reauth_invalid_user") + return self.async_update_reload_and_abort(reauth_entry, data=data) - return self.async_show_form( - step_id="reauth_validate", - data_schema=vol.Schema( - { - vol.Required( - CONF_BRAND, - default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND), - ): vol.In(BRANDS_WITHOUT_OAUTH), - vol.Required(CONF_PASSWORD): str, - } - ), - errors=errors, - description_placeholders=description_placeholders - | { - CONF_USERNAME: self._user_auth_details[CONF_USERNAME], - }, + # This is a one-time migration from username to user ID + # Only validate if the account has emails + emails: list[str] + if emails := decoded.get("email", []): + # Validate that the email matches before allowing migration + email_to_check_lower = reauth_entry.unique_id.casefold() + if not any(email.casefold() == email_to_check_lower for email in emails): + # Email doesn't match - this is a different account + return self.async_abort(reason="reauth_invalid_user") + + # Email matches or no emails on account, update with new unique ID + return self.async_update_reload_and_abort( + reauth_entry, data=data, unique_id=user_id ) - async def _async_reset_access_token_cache_if_needed( - self, gateway: AugustGateway, username: str, access_token_cache_file: str | None - ) -> None: - """Reset the access token cache if needed.""" - # We need to configure the access token cache file before we setup the gateway - # since we need to reset it if the brand changes BEFORE we setup the gateway - gateway.async_configure_access_token_cache_file( - username, access_token_cache_file - ) - if self._needs_reset: - self._needs_reset = False - await gateway.async_reset_authentication() + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: + """Create an entry for the flow.""" + # Decode JWT once + access_token = data["token"]["access_token"] + decoded = self._async_decode_jwt(access_token) + user_id = decoded["userId"] - async def _async_auth_or_validate(self) -> ValidateResult: - """Authenticate or validate.""" - user_auth_details = self._user_auth_details - gateway = self._async_get_gateway() - assert gateway is not None - await self._async_reset_access_token_cache_if_needed( - gateway, - user_auth_details[CONF_USERNAME], - user_auth_details.get(CONF_ACCESS_TOKEN_CACHE_FILE), - ) - await gateway.async_setup(user_auth_details) + if self.source == SOURCE_REAUTH: + return await self._async_handle_reauth(data, decoded, user_id) - errors: dict[str, str] = {} - info: dict[str, Any] = {} - description_placeholders: dict[str, str] = {} - validation_required = False - - try: - info = await async_validate_input(user_auth_details, gateway) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except RequireValidation: - validation_required = True - except Exception as ex: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unhandled" - description_placeholders = {"error": str(ex)} - - return ValidateResult( - validation_required, info, errors, description_placeholders - ) - - async def _async_update_or_create_entry( - self, info: dict[str, Any] - ) -> ConfigFlowResult: - """Update existing entry or create a new one.""" - self._async_shutdown_gateway() - - existing_entry = await self.async_set_unique_id( - self._user_auth_details[CONF_USERNAME] - ) - if not existing_entry: - return self.async_create_entry(title=info["title"], data=info["data"]) - - return self.async_update_reload_and_abort(existing_entry, data=info["data"]) + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + return await super().async_oauth_create_entry(data) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 661b291edb1..878d2fbdefc 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -1,5 +1,7 @@ """Constants for August devices.""" +from yalexs.const import Brand + from homeassistant.const import Platform DEFAULT_TIMEOUT = 25 @@ -9,6 +11,8 @@ CONF_BRAND = "brand" CONF_LOGIN_METHOD = "login_method" CONF_INSTALL_ID = "install_id" +DEFAULT_AUGUST_BRAND = Brand.YALE_AUGUST + VERIFICATION_CODE_KEY = "verification_code" NOTIFICATION_ID = "august_notification" diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 2c6ad739bdc..cc4e32c3993 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -1,30 +1,43 @@ """Handle August connection setup and authentication.""" -from typing import Any +import logging +from pathlib import Path -from yalexs.const import DEFAULT_BRAND +from aiohttp import ClientSession +from yalexs.authenticator_common import Authentication, AuthenticationState from yalexs.manager.gateway import Gateway -from homeassistant.const import CONF_USERNAME +from homeassistant.helpers import config_entry_oauth2_flow -from .const import ( - CONF_ACCESS_TOKEN_CACHE_FILE, - CONF_BRAND, - CONF_INSTALL_ID, - CONF_LOGIN_METHOD, -) +_LOGGER = logging.getLogger(__name__) class AugustGateway(Gateway): """Handle the connection to August.""" - def config_entry(self) -> dict[str, Any]: - """Config entry.""" - assert self._config is not None - return { - CONF_BRAND: self._config.get(CONF_BRAND, DEFAULT_BRAND), - CONF_LOGIN_METHOD: self._config[CONF_LOGIN_METHOD], - CONF_USERNAME: self._config[CONF_USERNAME], - CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID), - CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file, - } + def __init__( + self, + config_path: Path, + aiohttp_session: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Init the connection.""" + super().__init__(config_path, aiohttp_session) + self._oauth_session = oauth_session + + async def async_get_access_token(self) -> str: + """Get access token.""" + await self._oauth_session.async_ensure_token_valid() + return self._oauth_session.token["access_token"] + + async def async_refresh_access_token_if_needed(self) -> None: + """Refresh the access token if needed.""" + await self._oauth_session.async_ensure_token_valid() + + async def async_authenticate(self) -> Authentication: + """Authenticate with the details provided to setup.""" + await self._oauth_session.async_ensure_token_valid() + self.authentication = Authentication( + AuthenticationState.AUTHENTICATED, None, None, None + ) + return self.authentication diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 51c5225b894..1a310dd8241 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -3,6 +3,7 @@ "name": "August", "codeowners": ["@bdraco"], "config_flow": true, + "dependencies": ["application_credentials", "cloud"], "dhcp": [ { "hostname": "connect", @@ -28,5 +29,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"] + "requirements": ["yalexs==8.12.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index fbc746e939e..c2f351abdd8 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -6,42 +6,34 @@ } }, "config": { - "error": { - "unhandled": "Unhandled error: {error}", - "invalid_verification_code": "Invalid verification code", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]", + "data": { + "implementation": "[%key:common::config_flow::data::implementation%]" + }, + "data_description": { + "implementation": "[%key:common::config_flow::description::implementation%]" + } + } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", + "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "reauth_invalid_user": "Reauthenticate must use the same account." }, - "step": { - "validation": { - "title": "Two-factor authentication", - "data": { - "verification_code": "Verification code" - }, - "description": "Please check your {login_method} ({username}) and enter the verification code below. Codes may take a few minutes to arrive." - }, - "user_validate": { - "description": "It is recommended to use the 'email' login method as some brands may not work with the 'phone' method. If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'. If you choose the wrong brand, you may be able to authenticate initially; however, you will not be able to operate devices. If you are unsure of the brand, create the integration again and try another brand.", - "data": { - "brand": "Brand", - "login_method": "Login Method", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - }, - "title": "Set up an August account" - }, - "reauth_validate": { - "description": "Choose the correct brand for your device, and enter the password for {username}. If you choose the wrong brand, you may be able to authenticate initially; however, you will not be able to operate devices. If you are unsure of the brand, create the integration again and try another brand.", - "data": { - "brand": "[%key:component::august::config::step::user_validate::data::brand%]", - "password": "[%key:common::config_flow::data::password%]" - }, - "title": "Reauthenticate an August account" - } + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" } }, "entity": { diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index fe7ccededf2..69ae3eb65bd 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -199,23 +199,19 @@ class AuthProvidersView(HomeAssistantView): ) -def _prepare_result_json( - result: AuthFlowResult, -) -> AuthFlowResult: - """Convert result to JSON.""" +def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - data = result.copy() - data.pop("result") - data.pop("data") - return data + return { + key: val for key, val in result.items() if key not in ("result", "data") + } if result["type"] != data_entry_flow.FlowResultType.FORM: - return result + return result # type: ignore[return-value] - data = result.copy() - - if (schema := data["data_schema"]) is None: - data["data_schema"] = [] # type: ignore[typeddict-item] # json result type + data = dict(result) + if (schema := result["data_schema"]) is None: + data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 6c85f5b7f55..5b4a539b86f 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -149,20 +149,16 @@ def websocket_depose_mfa( hass.async_create_task(async_depose(msg)) -def _prepare_result_json( - result: data_entry_flow.FlowResult, -) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" +def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - return result.copy() - + return dict(result) if result["type"] != data_entry_flow.FlowResultType.FORM: - return result + return result # type: ignore[return-value] - data = result.copy() - - if (schema := data["data_schema"]) is None: - data["data_schema"] = [] # type: ignore[typeddict-item] # json result type + data = dict(result) + if (schema := result["data_schema"]) is None: + data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 10f7cb115da..a7bb8a0c550 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import timedelta import logging +API_ABS_HUMID = "abs_humid" API_CO2 = "carbon_dioxide" API_DEW_POINT = "dew_point" API_DUST = "dust" diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index d1f3ec34bf4..b0a44cb3e17 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONNECTIONS, ATTR_SW_VERSION, + CONCENTRATION_GRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, @@ -33,6 +34,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + API_ABS_HUMID, API_CO2, API_DEW_POINT, API_DUST, @@ -120,6 +122,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + AwairSensorEntityDescription( + key=API_ABS_HUMID, + device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY, + native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER, + unique_id_tag="absolute_humidity", + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), ) SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index 5086e44ab0f..531c9dac48e 100644 --- a/homeassistant/components/azure_devops/manifest.json +++ b/homeassistant/components/azure_devops/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/azure_devops", "iot_class": "cloud_polling", "loggers": ["aioazuredevops"], - "requirements": ["aioazuredevops==2.2.1"] + "requirements": ["aioazuredevops==2.2.2"] } diff --git a/homeassistant/components/backup/config.py b/homeassistant/components/backup/config.py index 0c8a5c82f7c..e4feb7dd8bd 100644 --- a/homeassistant/components/backup/config.py +++ b/homeassistant/components/backup/config.py @@ -127,7 +127,6 @@ class BackupConfigData: schedule=BackupSchedule( days=days, recurrence=ScheduleRecurrence(data["schedule"]["recurrence"]), - state=ScheduleState(data["schedule"].get("state", ScheduleState.NEVER)), time=time, ), ) @@ -453,7 +452,6 @@ class StoredBackupSchedule(TypedDict): days: list[Day] recurrence: ScheduleRecurrence - state: ScheduleState time: str | None @@ -462,7 +460,6 @@ class ScheduleParametersDict(TypedDict, total=False): days: list[Day] recurrence: ScheduleRecurrence - state: ScheduleState time: dt.time | None @@ -486,32 +483,12 @@ class ScheduleRecurrence(StrEnum): CUSTOM_DAYS = "custom_days" -class ScheduleState(StrEnum): - """Represent the schedule recurrence. - - This is deprecated and can be remove in HA Core 2025.8. - """ - - NEVER = "never" - DAILY = "daily" - MONDAY = "mon" - TUESDAY = "tue" - WEDNESDAY = "wed" - THURSDAY = "thu" - FRIDAY = "fri" - SATURDAY = "sat" - SUNDAY = "sun" - - @dataclass(kw_only=True) class BackupSchedule: """Represent the backup schedule.""" days: list[Day] = field(default_factory=list) recurrence: ScheduleRecurrence = ScheduleRecurrence.NEVER - # Although no longer used, state is kept for backwards compatibility. - # It can be removed in HA Core 2025.8. - state: ScheduleState = ScheduleState.NEVER time: dt.time | None = None cron_event: CronSim | None = field(init=False, default=None) next_automatic_backup: datetime | None = field(init=False, default=None) @@ -610,7 +587,6 @@ class BackupSchedule: return StoredBackupSchedule( days=self.days, recurrence=self.recurrence, - state=self.state, time=self.time.isoformat() if self.time else None, ) diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 3e6b13bfb56..d7e9b600155 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -331,9 +331,6 @@ async def handle_config_info( """Send the stored backup config.""" manager = hass.data[DATA_MANAGER] config = manager.config.data.to_dict() - # Remove state from schedule, it's not needed in the frontend - # mypy doesn't like deleting from TypedDict, ignore it - del config["schedule"]["state"] # type: ignore[misc] connection.send_result( msg["id"], { diff --git a/homeassistant/components/blue_current/manifest.json b/homeassistant/components/blue_current/manifest.json index 84604c62951..7c76657eb79 100644 --- a/homeassistant/components/blue_current/manifest.json +++ b/homeassistant/components/blue_current/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/blue_current", "iot_class": "cloud_push", "loggers": ["bluecurrent_api"], - "requirements": ["bluecurrent-api==1.2.4"] + "requirements": ["bluecurrent-api==1.3.1"] } diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 7abc929fde5..e3428eb9b86 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -388,12 +388,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE scanner = HaScanner(mode, adapter, address) scanner.async_setup() - try: - await scanner.async_start() - except (RuntimeError, ScannerStartError) as err: - raise ConfigEntryNotReady( - f"{adapter_human_name(adapter, address)}: {err}" - ) from err adapters = await manager.async_get_bluetooth_adapters() details = adapters[adapter] if entry.title == address: @@ -401,8 +395,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, title=adapter_title(adapter, details) ) slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS + # Register the scanner before starting so + # any raw advertisement data can be processed entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots)) await async_update_device(hass, entry, adapter, details) + try: + await scanner.async_start() + except (RuntimeError, ScannerStartError) as err: + raise ConfigEntryNotReady( + f"{adapter_human_name(adapter, address)}: {err}" + ) from err entry.async_on_unload(entry.add_update_listener(async_update_listener)) entry.async_on_unload(scanner.async_stop) return True diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 46c5425c730..5f3cb62c158 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -235,10 +235,9 @@ class HomeAssistantBluetoothManager(BluetoothManager): def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None: """Save the scanner history.""" - if isinstance(scanner, BaseHaRemoteScanner): - self.storage.async_set_advertisement_history( - scanner.source, scanner.serialize_discovered_devices() - ) + self.storage.async_set_advertisement_history( + scanner.source, scanner.serialize_discovered_devices() + ) def _async_unregister_scanner( self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE @@ -285,9 +284,8 @@ class HomeAssistantBluetoothManager(BluetoothManager): connection_slots: int | None = None, ) -> CALLBACK_TYPE: """Register a scanner.""" - if isinstance(scanner, BaseHaRemoteScanner): - if history := self.storage.async_get_advertisement_history(scanner.source): - scanner.restore_discovered_devices(history) + if history := self.storage.async_get_advertisement_history(scanner.source): + scanner.restore_discovered_devices(history) unregister = super().async_register_scanner(scanner, connection_slots) return partial(self._async_unregister_scanner, scanner, unregister) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index ce5d98f8edb..9efbd321123 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,11 +16,11 @@ "quality_scale": "internal", "requirements": [ "bleak==1.0.1", - "bleak-retry-connector==4.0.0", + "bleak-retry-connector==4.3.0", "bluetooth-adapters==2.0.0", "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.2", "dbus-fast==2.44.3", - "habluetooth==4.0.2" + "habluetooth==5.1.0" ] } diff --git a/homeassistant/components/bluetooth/websocket_api.py b/homeassistant/components/bluetooth/websocket_api.py index d21b11b050f..9022d98bf06 100644 --- a/homeassistant/components/bluetooth/websocket_api.py +++ b/homeassistant/components/bluetooth/websocket_api.py @@ -39,7 +39,13 @@ def async_setup(hass: HomeAssistant) -> None: def serialize_service_info( service_info: BluetoothServiceInfoBleak, time_diff: float ) -> dict[str, Any]: - """Serialize a BluetoothServiceInfoBleak object.""" + """Serialize a BluetoothServiceInfoBleak object. + + The raw field is included for: + 1. Debugging - to see the actual advertisement packet + 2. Data freshness - manufacturer_data and service_data are aggregated + across multiple advertisements, raw shows the latest packet only + """ return { "name": service_info.name, "address": service_info.address, @@ -57,6 +63,7 @@ def serialize_service_info( "connectable": service_info.connectable, "time": service_info.time + time_diff, "tx_power": service_info.tx_power, + "raw": service_info.raw.hex() if service_info.raw else None, } diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index 76c15a0a5c7..3adccda2ee5 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -95,7 +95,7 @@ "name": "Battery missing" }, "panel_fault_ac_fail": { - "name": "AC Failure" + "name": "AC failure" }, "panel_fault_parameter_crc_fail_in_pif": { "name": "CRC failure in panel configuration" diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 06ce45cdb3a..e0e2963c340 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -69,12 +69,7 @@ class SHCEntity(SHCBaseEntity): manufacturer=device.manufacturer, model=device.device_model, name=device.name, - via_device=( - DOMAIN, - device.parent_device_id - if device.parent_device_id is not None - else parent_id, - ), + via_device=(DOMAIN, device.root_device_id), ) super().__init__(device=device, parent_id=parent_id, entry_id=entry_id) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 0c99324efbb..bd2e127df3f 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/bosch_shc", "iot_class": "local_push", "loggers": ["boschshcpy"], - "requirements": ["boschshcpy==0.2.91"], + "requirements": ["boschshcpy==0.2.107"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/braviatv/button.py b/homeassistant/components/braviatv/button.py index 20250949bcb..a1ee159290a 100644 --- a/homeassistant/components/braviatv/button.py +++ b/homeassistant/components/braviatv/button.py @@ -53,8 +53,7 @@ async def async_setup_entry( assert unique_id is not None async_add_entities( - BraviaTVButton(coordinator, unique_id, config_entry.title, description) - for description in BUTTONS + BraviaTVButton(coordinator, unique_id, description) for description in BUTTONS ) @@ -67,11 +66,10 @@ class BraviaTVButton(BraviaTVEntity, ButtonEntity): self, coordinator: BraviaTVCoordinator, unique_id: str, - model: str, description: BraviaTVButtonDescription, ) -> None: """Initialize the button.""" - super().__init__(coordinator, unique_id, model) + super().__init__(coordinator, unique_id) self._attr_unique_id = f"{unique_id}_{description.key}" self.entity_description = description diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 5d775b98180..1a5aa1fddd6 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -79,14 +79,16 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN): system_info = await self.client.get_system_info() cid = system_info[ATTR_CID].lower() - title = system_info[ATTR_MODEL] self.device_config[CONF_MAC] = system_info[ATTR_MAC] await self.async_set_unique_id(cid) self._abort_if_unique_id_configured() - return self.async_create_entry(title=title, data=self.device_config) + return self.async_create_entry( + title=f"{system_info['name']} {system_info[ATTR_MODEL]}", + data=self.device_config, + ) async def async_reauth_device(self) -> ConfigFlowResult: """Reauthorize Bravia TV device from config.""" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 039726de94d..41b3923a716 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -81,6 +81,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.use_psk = config_entry.data.get(CONF_USE_PSK, False) self.client_id = config_entry.data.get(CONF_CLIENT_ID, LEGACY_CLIENT_ID) self.nickname = config_entry.data.get(CONF_NICKNAME, NICKNAME_PREFIX) + self.system_info: dict[str, str] = {} self.source: str | None = None self.source_list: list[str] = [] self.source_map: dict[str, dict] = {} @@ -150,6 +151,9 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.is_on = power_status == "active" self.skipped_updates = 0 + if not self.system_info: + self.system_info = await self.client.get_system_info() + if self.is_on is False: return diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py index b4e370f20d2..e1c6260b070 100644 --- a/homeassistant/components/braviatv/entity.py +++ b/homeassistant/components/braviatv/entity.py @@ -12,23 +12,16 @@ class BraviaTVEntity(CoordinatorEntity[BraviaTVCoordinator]): _attr_has_entity_name = True - def __init__( - self, - coordinator: BraviaTVCoordinator, - unique_id: str, - model: str, - ) -> None: + def __init__(self, coordinator: BraviaTVCoordinator, unique_id: str) -> None: """Initialize the entity.""" super().__init__(coordinator) self._attr_unique_id = unique_id self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, + connections={(CONNECTION_NETWORK_MAC, coordinator.system_info["macAddr"])}, manufacturer=ATTR_MANUFACTURER, - model=model, - name=f"{ATTR_MANUFACTURER} {model}", + model_id=coordinator.system_info["model"], + hw_version=coordinator.system_info["generation"], + serial_number=coordinator.system_info["serial"], ) - if coordinator.client.mac is not None: - self._attr_device_info["connections"] = { - (CONNECTION_NETWORK_MAC, coordinator.client.mac) - } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index fe9c386b060..c4226190ad8 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -34,9 +34,7 @@ async def async_setup_entry( unique_id = config_entry.unique_id assert unique_id is not None - async_add_entities( - [BraviaTVMediaPlayer(coordinator, unique_id, config_entry.title)] - ) + async_add_entities([BraviaTVMediaPlayer(coordinator, unique_id)]) class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 0611e367445..40f552c9258 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -24,7 +24,7 @@ async def async_setup_entry( unique_id = config_entry.unique_id assert unique_id is not None - async_add_entities([BraviaTVRemote(coordinator, unique_id, config_entry.title)]) + async_add_entities([BraviaTVRemote(coordinator, unique_id)]) class BraviaTVRemote(BraviaTVEntity, RemoteEntity): diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index deae818e2b5..356ba4f01fc 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"], - "requirements": ["brother==5.0.0"], + "requirements": ["brother==5.0.1"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 623bfbfef56..a7beb4f8d44 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -2,7 +2,16 @@ import dataclasses -from bsblan import BSBLAN, BSBLANConfig, Device, Info, StaticState +from bsblan import ( + BSBLAN, + BSBLANAuthError, + BSBLANConfig, + BSBLANConnectionError, + BSBLANError, + Device, + Info, + StaticState, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -13,9 +22,14 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_PASSKEY +from .const import CONF_PASSKEY, DOMAIN from .coordinator import BSBLanUpdateCoordinator PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER] @@ -54,10 +68,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo coordinator = BSBLanUpdateCoordinator(hass, entry, bsblan) await coordinator.async_config_entry_first_refresh() - # Fetch all required data concurrently - device = await bsblan.device() - info = await bsblan.info() - static = await bsblan.static_values() + try: + # Fetch all required data sequentially + device = await bsblan.device() + info = await bsblan.info() + static = await bsblan.static_values() + except BSBLANConnectionError as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="setup_connection_error", + translation_placeholders={"host": entry.data[CONF_HOST]}, + ) from err + except BSBLANAuthError as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="setup_auth_error", + ) from err + except BSBLANError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="setup_general_error", + ) from err entry.runtime_data = BSBLanData( client=bsblan, diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json index 86e52e76f41..b27be62e052 100644 --- a/homeassistant/components/bsblan/strings.json +++ b/homeassistant/components/bsblan/strings.json @@ -41,6 +41,11 @@ "passkey": "[%key:component::bsblan::config::step::user::data::passkey%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "passkey": "[%key:component::bsblan::config::step::user::data_description::passkey%]", + "username": "[%key:component::bsblan::config::step::user::data_description::username%]", + "password": "[%key:component::bsblan::config::step::user::data_description::password%]" } } }, @@ -66,6 +71,15 @@ }, "set_operation_mode_error": { "message": "An error occurred while setting the operation mode" + }, + "setup_connection_error": { + "message": "Failed to retrieve static device data from BSB-Lan device at {host}" + }, + "setup_auth_error": { + "message": "Authentication failed while retrieving static device data" + }, + "setup_general_error": { + "message": "An unknown error occurred while retrieving static device data" } }, "entity": { diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 7025929abd8..dbabad96041 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -72,7 +72,7 @@ SENSOR_DESCRIPTIONS = { key=str(BTHomeExtendedSensorDeviceClass.CHANNEL), state_class=SensorStateClass.MEASUREMENT, ), - # Conductivity (µS/cm) + # Conductivity (μS/cm) ( BTHomeSensorDeviceClass.CONDUCTIVITY, Units.CONDUCTIVITY, @@ -215,7 +215,7 @@ SENSOR_DESCRIPTIONS = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - # PM10 (µg/m3) + # PM10 (μg/m3) ( BTHomeSensorDeviceClass.PM10, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -225,7 +225,7 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), - # PM2.5 (µg/m3) + # PM2.5 (μg/m3) ( BTHomeSensorDeviceClass.PM25, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -318,7 +318,7 @@ SENSOR_DESCRIPTIONS = { key=str(BTHomeSensorDeviceClass.UV_INDEX), state_class=SensorStateClass.MEASUREMENT, ), - # Volatile organic Compounds (VOC) (µg/m3) + # Volatile organic Compounds (VOC) (μg/m3) ( BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, diff --git a/homeassistant/components/button/strings.json b/homeassistant/components/button/strings.json index f552e9ae12b..49a70ba9ffa 100644 --- a/homeassistant/components/button/strings.json +++ b/homeassistant/components/button/strings.json @@ -25,7 +25,7 @@ "services": { "press": { "name": "Press", - "description": "Press the button entity." + "description": "Presses a button entity." } } } diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 6c8b0536e2f..61c00e8dc8d 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,7 +14,7 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "iot_class": "local_polling", "loggers": ["casttube", "pychromecast"], - "requirements": ["PyChromecast==14.0.7"], + "requirements": ["PyChromecast==14.0.9"], "single_config_entry": true, "zeroconf": ["_googlecast._tcp.local."] } diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 790579d6a73..4a244ce3530 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -255,7 +255,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ) entity_description: ClimateEntityDescription - _attr_current_humidity: int | None = None + _attr_current_humidity: float | None = None _attr_current_temperature: float | None = None _attr_fan_mode: str | None _attr_fan_modes: list[str] | None diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 5bd40eb5b83..26fda1a405f 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -6,12 +6,16 @@ import asyncio from collections.abc import Callable from contextlib import suppress from datetime import datetime, timedelta -from http import HTTPStatus import logging from typing import TYPE_CHECKING, Any import aiohttp -from hass_nabucasa import Cloud, cloud_api +from hass_nabucasa import AlexaApiError, Cloud +from hass_nabucasa.alexa_api import ( + AlexaAccessTokenDetails, + AlexaApiNeedsRelinkError, + AlexaApiNoTokenError, +) from yarl import URL from homeassistant.components import persistent_notification @@ -146,7 +150,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): self._cloud_user = cloud_user self._prefs = prefs self._cloud = cloud - self._token = None + self._token: str | None = None self._token_valid: datetime | None = None self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA) self._alexa_sync_unsub: Callable[[], None] | None = None @@ -318,32 +322,31 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): async def async_get_access_token(self) -> str | None: """Get an access token.""" + details: AlexaAccessTokenDetails | None if self._token_valid is not None and self._token_valid > utcnow(): return self._token - resp = await cloud_api.async_alexa_access_token(self._cloud) - body = await resp.json() + try: + details = await self._cloud.alexa_api.access_token() + except AlexaApiNeedsRelinkError as exception: + if self.should_report_state: + persistent_notification.async_create( + self.hass, + ( + "There was an error reporting state to Alexa" + f" ({exception.reason}). Please re-link your Alexa skill via" + " the Alexa app to continue using it." + ), + "Alexa state reporting disabled", + "cloud_alexa_report", + ) + raise alexa_errors.RequireRelink from exception + except (AlexaApiNoTokenError, AlexaApiError) as exception: + raise alexa_errors.NoTokenAvailable from exception - if resp.status == HTTPStatus.BAD_REQUEST: - if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"): - if self.should_report_state: - persistent_notification.async_create( - self.hass, - ( - "There was an error reporting state to Alexa" - f" ({body['reason']}). Please re-link your Alexa skill via" - " the Alexa app to continue using it." - ), - "Alexa state reporting disabled", - "cloud_alexa_report", - ) - raise alexa_errors.RequireRelink - - raise alexa_errors.NoTokenAvailable - - self._token = body["access_token"] - self._endpoint = body["event_endpoint"] - self._token_valid = utcnow() + timedelta(seconds=body["expires_in"]) + self._token = details["access_token"] + self._endpoint = details["event_endpoint"] + self._token_valid = utcnow() + timedelta(seconds=details["expires_in"]) return self._token async def _async_prefs_updated(self, prefs: CloudPreferences) -> None: diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 2b6f45ec474..62496906c9d 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -7,7 +7,7 @@ from http import HTTPStatus import logging from typing import TYPE_CHECKING, Any -from hass_nabucasa import Cloud, cloud_api +from hass_nabucasa import Cloud from hass_nabucasa.google_report_state import ErrorResponse from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -377,7 +377,7 @@ class CloudGoogleConfig(AbstractConfig): return HTTPStatus.OK async with self._sync_entities_lock: - resp = await cloud_api.async_google_actions_request_sync(self._cloud) + resp = await self._cloud.google_report_state.request_sync() return resp.status async def async_connect_agent_user(self, agent_user_id: str) -> None: diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 76e55bc19b3..a0f88b3a558 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.111.1"], + "requirements": ["hass-nabucasa==1.0.0"], "single_config_entry": true } diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 317759f820d..dca7f774331 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -7,22 +7,18 @@ import logging from coinbase.rest import RESTClient from coinbase.rest.rest_base import HTTPError -from coinbase.wallet.client import Client as LegacyClient -from coinbase.wallet.error import AuthenticationError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.util import Throttle from .const import ( ACCOUNT_IS_VAULT, API_ACCOUNT_AMOUNT, API_ACCOUNT_AVALIABLE, - API_ACCOUNT_BALANCE, API_ACCOUNT_CURRENCY, - API_ACCOUNT_CURRENCY_CODE, API_ACCOUNT_HOLD, API_ACCOUNT_ID, API_ACCOUNT_NAME, @@ -31,12 +27,9 @@ from .const import ( API_DATA, API_RATES_CURRENCY, API_RESOURCE_TYPE, - API_TYPE_VAULT, API_V3_ACCOUNT_ID, API_V3_TYPE_VAULT, - CONF_CURRENCIES, CONF_EXCHANGE_BASE, - CONF_EXCHANGE_RATES, ) _LOGGER = logging.getLogger(__name__) @@ -51,9 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: CoinbaseConfigEntry) -> """Set up Coinbase from a config entry.""" instance = await hass.async_add_executor_job(create_and_update_instance, entry) - - entry.async_on_unload(entry.add_update_listener(update_listener)) - entry.runtime_data = instance await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -68,68 +58,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: CoinbaseConfigEntry) -> def create_and_update_instance(entry: CoinbaseConfigEntry) -> CoinbaseData: """Create and update a Coinbase Data instance.""" + + # Check if user is using deprecated v2 API credentials if "organizations" not in entry.data[CONF_API_KEY]: - client = LegacyClient(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN]) - version = "v2" - else: - client = RESTClient( - api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN] + # Trigger reauthentication to ask user for v3 credentials + raise ConfigEntryAuthFailed( + "Your Coinbase API key appears to be for the deprecated v2 API. " + "Please reconfigure with a new API key created for the v3 API. " + "Visit https://www.coinbase.com/developer-platform to create new credentials." ) - version = "v3" + + client = RESTClient( + api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN] + ) base_rate = entry.options.get(CONF_EXCHANGE_BASE, "USD") - instance = CoinbaseData(client, base_rate, version) + instance = CoinbaseData(client, base_rate) instance.update() return instance -async def update_listener( - hass: HomeAssistant, config_entry: CoinbaseConfigEntry -) -> None: - """Handle options update.""" - - await hass.config_entries.async_reload(config_entry.entry_id) - - registry = er.async_get(hass) - entities = er.async_entries_for_config_entry(registry, config_entry.entry_id) - - # Remove orphaned entities - for entity in entities: - currency = entity.unique_id.split("-")[-1] - if ( - "xe" in entity.unique_id - and currency not in config_entry.options.get(CONF_EXCHANGE_RATES, []) - ) or ( - "wallet" in entity.unique_id - and currency not in config_entry.options.get(CONF_CURRENCIES, []) - ): - registry.async_remove(entity.entity_id) - - -def get_accounts(client, version): +def get_accounts(client): """Handle paginated accounts.""" response = client.get_accounts() - if version == "v2": - accounts = response[API_DATA] - next_starting_after = response.pagination.next_starting_after - - while next_starting_after: - response = client.get_accounts(starting_after=next_starting_after) - accounts += response[API_DATA] - next_starting_after = response.pagination.next_starting_after - - return [ - { - API_ACCOUNT_ID: account[API_ACCOUNT_ID], - API_ACCOUNT_NAME: account[API_ACCOUNT_NAME], - API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY][ - API_ACCOUNT_CURRENCY_CODE - ], - API_ACCOUNT_AMOUNT: account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT], - ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_TYPE_VAULT, - } - for account in accounts - ] - accounts = response[API_ACCOUNTS] while response["has_next"]: response = client.get_accounts(cursor=response["cursor"]) @@ -153,37 +103,28 @@ def get_accounts(client, version): class CoinbaseData: """Get the latest data and update the states.""" - def __init__(self, client, exchange_base, version): + def __init__(self, client, exchange_base): """Init the coinbase data object.""" self.client = client self.accounts = None self.exchange_base = exchange_base self.exchange_rates = None - if version == "v2": - self.user_id = self.client.get_current_user()[API_ACCOUNT_ID] - else: - self.user_id = ( - "v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID] - ) - self.api_version = version + self.user_id = ( + "v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID] + ) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from coinbase.""" try: - self.accounts = get_accounts(self.client, self.api_version) - if self.api_version == "v2": - self.exchange_rates = self.client.get_exchange_rates( - currency=self.exchange_base - ) - else: - self.exchange_rates = self.client.get( - "/v2/exchange-rates", - params={API_RATES_CURRENCY: self.exchange_base}, - )[API_DATA] - except (AuthenticationError, HTTPError) as coinbase_error: + self.accounts = get_accounts(self.client) + self.exchange_rates = self.client.get( + "/v2/exchange-rates", + params={API_RATES_CURRENCY: self.exchange_base}, + )[API_DATA] + except HTTPError as coinbase_error: _LOGGER.error( "Authentication error connecting to coinbase: %s", coinbase_error ) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 3234ec29679..6aad3a81d17 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -2,17 +2,20 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from coinbase.rest import RESTClient from coinbase.rest.rest_base import HTTPError -from coinbase.wallet.client import Client as LegacyClient -from coinbase.wallet.error import AuthenticationError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION +from homeassistant.config_entries import ( + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithReload, +) +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -45,9 +48,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema( def get_user_from_client(api_key, api_token): """Get the user name from Coinbase API credentials.""" - if "organizations" not in api_key: - client = LegacyClient(api_key, api_token) - return client.get_current_user()["name"] client = RESTClient(api_key=api_key, api_secret=api_token) return client.get_portfolios()["portfolios"][0]["name"] @@ -59,7 +59,7 @@ async def validate_api(hass: HomeAssistant, data): user = await hass.async_add_executor_job( get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN] ) - except (AuthenticationError, HTTPError) as error: + except HTTPError as error: if "api key" in str(error) or " 401 Client Error" in str(error): _LOGGER.debug("Coinbase rejected API credentials due to an invalid API key") raise InvalidKey from error @@ -74,8 +74,8 @@ async def validate_api(hass: HomeAssistant, data): raise InvalidAuth from error except ConnectionError as error: raise CannotConnect from error - api_version = "v3" if "organizations" in data[CONF_API_KEY] else "v2" - return {"title": user, "api_version": api_version} + + return {"title": user} async def validate_options( @@ -85,20 +85,17 @@ async def validate_options( client = config_entry.runtime_data.client - accounts = await hass.async_add_executor_job( - get_accounts, client, config_entry.data.get("api_version", "v2") - ) + accounts = await hass.async_add_executor_job(get_accounts, client) accounts_currencies = [ account[API_ACCOUNT_CURRENCY] for account in accounts if not account[ACCOUNT_IS_VAULT] ] - if config_entry.data.get("api_version", "v2") == "v2": - available_rates = await hass.async_add_executor_job(client.get_exchange_rates) - else: - resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates") - available_rates = resp[API_DATA] + + resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates") + available_rates = resp[API_DATA] + if CONF_CURRENCIES in options: for currency in options[CONF_CURRENCIES]: if currency not in accounts_currencies: @@ -117,6 +114,8 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + reauth_entry: CoinbaseConfigEntry + async def async_step_user( self, user_input: dict[str, str] | None = None ) -> ConfigFlowResult: @@ -143,12 +142,63 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - user_input[CONF_API_VERSION] = info["api_version"] return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauthentication flow.""" + self.reauth_entry = self._get_reauth_entry() + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Handle reauthentication confirmation.""" + errors: dict[str, str] = {} + + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_USER_DATA_SCHEMA, + description_placeholders={ + "account_name": self.reauth_entry.title, + }, + errors=errors, + ) + + try: + await validate_api(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidKey: + errors["base"] = "invalid_auth_key" + except InvalidSecret: + errors["base"] = "invalid_auth_secret" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + self.reauth_entry, + data_updates=user_input, + reason="reauth_successful", + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_USER_DATA_SCHEMA, + description_placeholders={ + "account_name": self.reauth_entry.title, + }, + errors=errors, + ) + @staticmethod @callback def async_get_options_flow( @@ -158,7 +208,7 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN): return OptionsFlowHandler() -class OptionsFlowHandler(OptionsFlow): +class OptionsFlowHandler(OptionsFlowWithReload): """Handle a option flow for Coinbase.""" async def async_step_init( diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index be632b5e856..fcd48f9e91d 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/coinbase", "iot_class": "cloud_polling", "loggers": ["coinbase"], - "requirements": ["coinbase==2.1.0", "coinbase-advanced-py==1.2.2"] + "requirements": ["coinbase-advanced-py==1.2.2"] } diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 578877e7d90..4dfc744b7fa 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -6,6 +6,7 @@ import logging from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -27,7 +28,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) ATTR_NATIVE_BALANCE = "Balance in native currency" -ATTR_API_VERSION = "API Version" CURRENCY_ICONS = { "BTC": "mdi:currency-btc", @@ -69,11 +69,26 @@ async def async_setup_entry( CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_PRECISION_DEFAULT ) + # Remove orphaned entities + registry = er.async_get(hass) + existing_entities = er.async_entries_for_config_entry( + registry, config_entry.entry_id + ) + for entity in existing_entities: + currency = entity.unique_id.split("-")[-1] + if ( + "xe" in entity.unique_id + and currency not in config_entry.options.get(CONF_EXCHANGE_RATES, []) + ) or ( + "wallet" in entity.unique_id + and currency not in config_entry.options.get(CONF_CURRENCIES, []) + ): + registry.async_remove(entity.entity_id) + for currency in desired_currencies: _LOGGER.debug( - "Attempting to set up %s account sensor with %s API", + "Attempting to set up %s account sensor", currency, - instance.api_version, ) if currency not in provided_currencies: _LOGGER.warning( @@ -89,9 +104,8 @@ async def async_setup_entry( if CONF_EXCHANGE_RATES in config_entry.options: for rate in config_entry.options[CONF_EXCHANGE_RATES]: _LOGGER.debug( - "Attempting to set up %s account sensor with %s API", + "Attempting to set up %s exchange rate sensor", rate, - instance.api_version, ) entities.append( ExchangeRateSensor( @@ -146,15 +160,13 @@ class AccountSensor(SensorEntity): """Return the state attributes of the sensor.""" return { ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}", - ATTR_API_VERSION: self._coinbase_data.api_version, } def update(self) -> None: """Get the latest state of the sensor.""" _LOGGER.debug( - "Updating %s account sensor with %s API", + "Updating %s account sensor", self._currency, - self._coinbase_data.api_version, ) self._coinbase_data.update() for account in self._coinbase_data.accounts: @@ -210,9 +222,8 @@ class ExchangeRateSensor(SensorEntity): def update(self) -> None: """Get the latest state of the sensor.""" _LOGGER.debug( - "Updating %s rate sensor with %s API", + "Updating %s rate sensor", self._currency, - self._coinbase_data.api_version, ) self._coinbase_data.update() self._attr_native_value = round( diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json index 74510731b7a..b0774baf403 100644 --- a/homeassistant/components/coinbase/strings.json +++ b/homeassistant/components/coinbase/strings.json @@ -8,6 +8,14 @@ "api_key": "[%key:common::config_flow::data::api_key%]", "api_token": "API secret" } + }, + "reauth_confirm": { + "title": "Update Coinbase API credentials", + "description": "Your current Coinbase API key appears to be for the deprecated v2 API. Please reconfigure with a new API key created for the v3 API. Visit https://www.coinbase.com/developer-platform to create new credentials for {account_name}.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "api_token": "API secret" + } } }, "error": { @@ -18,7 +26,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "Successfully updated credentials" } }, "options": { diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index a9aafcfaa5e..176c9e2b047 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -137,20 +137,16 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView): def _prepare_config_flow_result_json( result: data_entry_flow.FlowResult, - prepare_result_json: Callable[ - [data_entry_flow.FlowResult], data_entry_flow.FlowResult - ], -) -> data_entry_flow.FlowResult: + prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]], +) -> dict[str, Any]: """Convert result to JSON.""" if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: return prepare_result_json(result) - data = result.copy() - entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item] + data = {key: val for key, val in result.items() if key not in ("data", "context")} + entry: config_entries.ConfigEntry = result["result"] # type: ignore[typeddict-item] # We overwrite the ConfigEntry object with its json representation. - data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key] - data.pop("data") - data.pop("context") + data["result"] = entry.as_json_fragment return data @@ -204,8 +200,8 @@ class ConfigManagerFlowIndexView( def _prepare_result_json( self, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" + ) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" return _prepare_config_flow_result_json(result, super()._prepare_result_json) @@ -229,8 +225,8 @@ class ConfigManagerFlowResourceView( def _prepare_result_json( self, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" + ) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" return _prepare_config_flow_result_json(result, super()._prepare_result_json) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 3435a7d2ed4..dec26dd3215 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -40,6 +40,7 @@ from .chat_log import ( ConverseError, SystemContent, ToolResultContent, + ToolResultContentDeltaDict, UserContent, async_get_chat_log, ) @@ -79,6 +80,7 @@ __all__ = [ "ConverseError", "SystemContent", "ToolResultContent", + "ToolResultContentDeltaDict", "UserContent", "async_conversation_trace_append", "async_converse", @@ -117,7 +119,7 @@ CONFIG_SCHEMA = vol.Schema( {cv.string: vol.All(cv.ensure_list, [cv.string])} ) } - ) + ), }, extra=vol.ALLOW_EXTRA, ) @@ -268,8 +270,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entity_component = EntityComponent[ConversationEntity](_LOGGER, DOMAIN, hass) hass.data[DATA_COMPONENT] = entity_component + agent_config = config.get(DOMAIN, {}) await async_setup_default_agent( - hass, entity_component, config.get(DOMAIN, {}).get("intents", {}) + hass, entity_component, config_intents=agent_config.get("intents", {}) ) async def handle_process(service: ServiceCall) -> ServiceResponse: diff --git a/homeassistant/components/conversation/chat_log.py b/homeassistant/components/conversation/chat_log.py index 648a89e47f1..2f5e3b0cf82 100644 --- a/homeassistant/components/conversation/chat_log.py +++ b/homeassistant/components/conversation/chat_log.py @@ -9,7 +9,7 @@ from contextvars import ContextVar from dataclasses import asdict, dataclass, field, replace import logging from pathlib import Path -from typing import Any, Literal, TypedDict +from typing import Any, Literal, TypedDict, cast import voluptuous as vol @@ -161,7 +161,9 @@ class AssistantContent: role: Literal["assistant"] = field(init=False, default="assistant") agent_id: str content: str | None = None + thinking_content: str | None = None tool_calls: list[llm.ToolInput] | None = None + native: Any = None @dataclass(frozen=True) @@ -183,7 +185,18 @@ class AssistantContentDeltaDict(TypedDict, total=False): role: Literal["assistant"] content: str | None + thinking_content: str | None tool_calls: list[llm.ToolInput] | None + native: Any + + +class ToolResultContentDeltaDict(TypedDict, total=False): + """Tool result content.""" + + role: Literal["tool_result"] + tool_call_id: str + tool_name: str + tool_result: JsonObjectType @dataclass @@ -231,17 +244,25 @@ class ChatLog: @callback def async_add_assistant_content_without_tools( - self, content: AssistantContent + self, content: AssistantContent | ToolResultContent ) -> None: - """Add assistant content to the log.""" + """Add assistant content to the log. + + Allows assistant content without tool calls or with external tool calls only, + as well as tool results for the external tools. + """ LOGGER.debug("Adding assistant content: %s", content) - if content.tool_calls is not None: - raise ValueError("Tool calls not allowed") + if ( + isinstance(content, AssistantContent) + and content.tool_calls is not None + and any(not tool_call.external for tool_call in content.tool_calls) + ): + raise ValueError("Non-external tool calls not allowed") self.content.append(content) async def async_add_assistant_content( self, - content: AssistantContent, + content: AssistantContent | ToolResultContent, /, tool_call_tasks: dict[str, asyncio.Task] | None = None, ) -> AsyncGenerator[ToolResultContent]: @@ -254,7 +275,11 @@ class ChatLog: LOGGER.debug("Adding assistant content: %s", content) self.content.append(content) - if content.tool_calls is None: + if ( + not isinstance(content, AssistantContent) + or content.tool_calls is None + or all(tool_call.external for tool_call in content.tool_calls) + ): return if self.llm_api is None: @@ -263,13 +288,16 @@ class ChatLog: if tool_call_tasks is None: tool_call_tasks = {} for tool_input in content.tool_calls: - if tool_input.id not in tool_call_tasks: + if tool_input.id not in tool_call_tasks and not tool_input.external: tool_call_tasks[tool_input.id] = self.hass.async_create_task( self.llm_api.async_call_tool(tool_input), name=f"llm_tool_{tool_input.id}", ) for tool_input in content.tool_calls: + if tool_input.external: + continue + LOGGER.debug( "Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args ) @@ -292,7 +320,9 @@ class ChatLog: yield response_content async def async_add_delta_content_stream( - self, agent_id: str, stream: AsyncIterable[AssistantContentDeltaDict] + self, + agent_id: str, + stream: AsyncIterable[AssistantContentDeltaDict | ToolResultContentDeltaDict], ) -> AsyncGenerator[AssistantContent | ToolResultContent]: """Stream content into the chat log. @@ -306,6 +336,8 @@ class ChatLog: The keys content and tool_calls will be concatenated if they appear multiple times. """ current_content = "" + current_thinking_content = "" + current_native: Any = None current_tool_calls: list[llm.ToolInput] = [] tool_call_tasks: dict[str, asyncio.Task] = {} @@ -314,34 +346,54 @@ class ChatLog: # Indicates update to current message if "role" not in delta: - if delta_content := delta.get("content"): + # ToolResultContentDeltaDict will always have a role + assistant_delta = cast(AssistantContentDeltaDict, delta) + if delta_content := assistant_delta.get("content"): current_content += delta_content - if delta_tool_calls := delta.get("tool_calls"): - if self.llm_api is None: - raise ValueError("No LLM API configured") + if delta_thinking_content := assistant_delta.get("thinking_content"): + current_thinking_content += delta_thinking_content + if delta_native := assistant_delta.get("native"): + if current_native is not None: + raise RuntimeError( + "Native content already set, cannot overwrite" + ) + current_native = delta_native + if delta_tool_calls := assistant_delta.get("tool_calls"): current_tool_calls += delta_tool_calls # Start processing the tool calls as soon as we know about them for tool_call in delta_tool_calls: - tool_call_tasks[tool_call.id] = self.hass.async_create_task( - self.llm_api.async_call_tool(tool_call), - name=f"llm_tool_{tool_call.id}", - ) + if not tool_call.external: + if self.llm_api is None: + raise ValueError("No LLM API configured") + + tool_call_tasks[tool_call.id] = self.hass.async_create_task( + self.llm_api.async_call_tool(tool_call), + name=f"llm_tool_{tool_call.id}", + ) if self.delta_listener: - self.delta_listener(self, delta) # type: ignore[arg-type] + if filtered_delta := { + k: v for k, v in assistant_delta.items() if k != "native" + }: + # We do not want to send the native content to the listener + # as it is not JSON serializable + self.delta_listener(self, filtered_delta) continue # Starting a new message - - if delta["role"] != "assistant": - raise ValueError(f"Only assistant role expected. Got {delta['role']}") - # Yield the previous message if it has content - if current_content or current_tool_calls: - content = AssistantContent( + if ( + current_content + or current_thinking_content + or current_tool_calls + or current_native + ): + content: AssistantContent | ToolResultContent = AssistantContent( agent_id=agent_id, content=current_content or None, + thinking_content=current_thinking_content or None, tool_calls=current_tool_calls or None, + native=current_native, ) yield content async for tool_result in self.async_add_assistant_content( @@ -350,18 +402,51 @@ class ChatLog: yield tool_result if self.delta_listener: self.delta_listener(self, asdict(tool_result)) + current_content = "" + current_thinking_content = "" + current_native = None + current_tool_calls = [] - current_content = delta.get("content") or "" - current_tool_calls = delta.get("tool_calls") or [] + if delta["role"] == "assistant": + current_content = delta.get("content") or "" + current_thinking_content = delta.get("thinking_content") or "" + current_tool_calls = delta.get("tool_calls") or [] + current_native = delta.get("native") - if self.delta_listener: - self.delta_listener(self, delta) # type: ignore[arg-type] + if self.delta_listener: + if filtered_delta := { + k: v for k, v in delta.items() if k != "native" + }: + self.delta_listener(self, filtered_delta) + elif delta["role"] == "tool_result": + content = ToolResultContent( + agent_id=agent_id, + tool_call_id=delta["tool_call_id"], + tool_name=delta["tool_name"], + tool_result=delta["tool_result"], + ) + yield content + if self.delta_listener: + self.delta_listener(self, asdict(content)) + self.async_add_assistant_content_without_tools(content) + else: + raise ValueError( + "Only assistant and tool_result roles expected." + f" Got {delta['role']}" + ) - if current_content or current_tool_calls: + if ( + current_content + or current_thinking_content + or current_tool_calls + or current_native + ): content = AssistantContent( agent_id=agent_id, content=current_content or None, + thinking_content=current_thinking_content or None, tool_calls=current_tool_calls or None, + native=current_native, ) yield content async for tool_result in self.async_add_assistant_content( diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index bed4b4c0dd6..4b056ead2c2 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -14,14 +14,19 @@ import re import time from typing import IO, Any, cast -from hassil.expression import Expression, ListReference, Sequence, TextChunk +from hassil.expression import Expression, Group, ListReference, TextChunk +from hassil.fuzzy import FuzzyNgramMatcher, SlotCombinationInfo from hassil.intents import ( + Intent, + IntentData, Intents, SlotList, TextSlotList, TextSlotValue, WildcardSlotList, ) +from hassil.models import MatchEntity +from hassil.ngram import Sqlite3NgramModel from hassil.recognize import ( MISSING_ENTITY, RecognizeResult, @@ -31,7 +36,15 @@ from hassil.recognize import ( from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity from hassil.trie import Trie from hassil.util import merge_dict -from home_assistant_intents import ErrorKey, get_intents, get_languages +from home_assistant_intents import ( + ErrorKey, + FuzzyConfig, + FuzzyLanguageResponses, + get_fuzzy_config, + get_fuzzy_language, + get_intents, + get_languages, +) import yaml from homeassistant import core @@ -76,6 +89,7 @@ TRIGGER_CALLBACK_TYPE = Callable[ ] METADATA_CUSTOM_SENTENCE = "hass_custom_sentence" METADATA_CUSTOM_FILE = "hass_custom_file" +METADATA_FUZZY_MATCH = "hass_fuzzy_match" ERROR_SENTINEL = object() @@ -94,6 +108,8 @@ class LanguageIntents: intent_responses: dict[str, Any] error_responses: dict[str, Any] language_variant: str | None + fuzzy_matcher: FuzzyNgramMatcher | None = None + fuzzy_responses: FuzzyLanguageResponses | None = None @dataclass(slots=True) @@ -119,10 +135,13 @@ class IntentMatchingStage(Enum): EXPOSED_ENTITIES_ONLY = auto() """Match against exposed entities only.""" + FUZZY = auto() + """Use fuzzy matching to guess intent.""" + UNEXPOSED_ENTITIES = auto() """Match against unexposed entities in Home Assistant.""" - FUZZY = auto() + UNKNOWN_NAMES = auto() """Capture names that are not known to Home Assistant.""" @@ -241,6 +260,10 @@ class DefaultAgent(ConversationEntity): # LRU cache to avoid unnecessary intent matching self._intent_cache = IntentCache(capacity=128) + # Shared configuration for fuzzy matching + self.fuzzy_matching = True + self._fuzzy_config: FuzzyConfig | None = None + @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" @@ -299,7 +322,7 @@ class DefaultAgent(ConversationEntity): _LOGGER.warning("No intents were loaded for language: %s", language) return None - slot_lists = self._make_slot_lists() + slot_lists = await self._make_slot_lists() intent_context = self._make_intent_context(user_input) if self._exposed_names_trie is not None: @@ -556,6 +579,36 @@ class DefaultAgent(ConversationEntity): # Don't try matching against all entities or doing a fuzzy match return None + # Use fuzzy matching + skip_fuzzy_match = False + if cache_value is not None: + if (cache_value.result is not None) and ( + cache_value.stage == IntentMatchingStage.FUZZY + ): + _LOGGER.debug("Got cached result for fuzzy match") + return cache_value.result + + # Continue with matching, but we know we won't succeed for fuzzy + # match. + skip_fuzzy_match = True + + if (not skip_fuzzy_match) and self.fuzzy_matching: + start_time = time.monotonic() + fuzzy_result = self._recognize_fuzzy(lang_intents, user_input) + + # Update cache + self._intent_cache.put( + cache_key, + IntentCacheValue(result=fuzzy_result, stage=IntentMatchingStage.FUZZY), + ) + + _LOGGER.debug( + "Did fuzzy match in %s second(s)", time.monotonic() - start_time + ) + + if fuzzy_result is not None: + return fuzzy_result + # Try again with all entities (including unexposed) skip_unexposed_entities_match = False if cache_value is not None: @@ -601,102 +654,160 @@ class DefaultAgent(ConversationEntity): # This should fail the intent handling phase (async_match_targets). return strict_result - # Try again with missing entities enabled - skip_fuzzy_match = False + # Check unknown names + skip_unknown_names = False if cache_value is not None: if (cache_value.result is not None) and ( - cache_value.stage == IntentMatchingStage.FUZZY + cache_value.stage == IntentMatchingStage.UNKNOWN_NAMES ): - _LOGGER.debug("Got cached result for fuzzy match") + _LOGGER.debug("Got cached result for unknown names") return cache_value.result - # We know we won't succeed for fuzzy matching. - skip_fuzzy_match = True + skip_unknown_names = True maybe_result: RecognizeResult | None = None - if not skip_fuzzy_match: + if not skip_unknown_names: start_time = time.monotonic() - best_num_matched_entities = 0 - best_num_unmatched_entities = 0 - best_num_unmatched_ranges = 0 - for result in recognize_all( - user_input.text, - lang_intents.intents, - slot_lists=slot_lists, - intent_context=intent_context, - allow_unmatched_entities=True, - ): - if result.text_chunks_matched < 1: - # Skip results that don't match any literal text - continue - - # Don't count missing entities that couldn't be filled from context - num_matched_entities = 0 - for matched_entity in result.entities_list: - if matched_entity.name not in result.unmatched_entities: - num_matched_entities += 1 - - num_unmatched_entities = 0 - num_unmatched_ranges = 0 - for unmatched_entity in result.unmatched_entities_list: - if isinstance(unmatched_entity, UnmatchedTextEntity): - if unmatched_entity.text != MISSING_ENTITY: - num_unmatched_entities += 1 - elif isinstance(unmatched_entity, UnmatchedRangeEntity): - num_unmatched_ranges += 1 - num_unmatched_entities += 1 - else: - num_unmatched_entities += 1 - - if ( - (maybe_result is None) # first result - or ( - # More literal text matched - result.text_chunks_matched > maybe_result.text_chunks_matched - ) - or ( - # More entities matched - num_matched_entities > best_num_matched_entities - ) - or ( - # Fewer unmatched entities - (num_matched_entities == best_num_matched_entities) - and (num_unmatched_entities < best_num_unmatched_entities) - ) - or ( - # Prefer unmatched ranges - (num_matched_entities == best_num_matched_entities) - and (num_unmatched_entities == best_num_unmatched_entities) - and (num_unmatched_ranges > best_num_unmatched_ranges) - ) - or ( - # Prefer match failures with entities - (result.text_chunks_matched == maybe_result.text_chunks_matched) - and (num_unmatched_entities == best_num_unmatched_entities) - and (num_unmatched_ranges == best_num_unmatched_ranges) - and ( - ("name" in result.entities) - or ("name" in result.unmatched_entities) - ) - ) - ): - maybe_result = result - best_num_matched_entities = num_matched_entities - best_num_unmatched_entities = num_unmatched_entities - best_num_unmatched_ranges = num_unmatched_ranges + maybe_result = self._recognize_unknown_names( + lang_intents, user_input, slot_lists, intent_context + ) # Update cache self._intent_cache.put( cache_key, - IntentCacheValue(result=maybe_result, stage=IntentMatchingStage.FUZZY), + IntentCacheValue( + result=maybe_result, stage=IntentMatchingStage.UNKNOWN_NAMES + ), ) _LOGGER.debug( - "Did fuzzy match in %s second(s)", time.monotonic() - start_time + "Did unknown names match in %s second(s)", time.monotonic() - start_time ) return maybe_result + def _recognize_fuzzy( + self, lang_intents: LanguageIntents, user_input: ConversationInput + ) -> RecognizeResult | None: + """Return fuzzy recognition from hassil.""" + if lang_intents.fuzzy_matcher is None: + return None + + fuzzy_result = lang_intents.fuzzy_matcher.match(user_input.text) + if fuzzy_result is None: + return None + + response = "default" + if lang_intents.fuzzy_responses: + domain = "" # no domain + if "name" in fuzzy_result.slots: + domain = fuzzy_result.name_domain + elif "domain" in fuzzy_result.slots: + domain = fuzzy_result.slots["domain"].value + + slot_combo = tuple(sorted(fuzzy_result.slots)) + if ( + intent_responses := lang_intents.fuzzy_responses.get( + fuzzy_result.intent_name + ) + ) and (combo_responses := intent_responses.get(slot_combo)): + response = combo_responses.get(domain, response) + + entities = [ + MatchEntity(name=slot_name, value=slot_value.value, text=slot_value.text) + for slot_name, slot_value in fuzzy_result.slots.items() + ] + + return RecognizeResult( + intent=Intent(name=fuzzy_result.intent_name), + intent_data=IntentData(sentence_texts=[]), + intent_metadata={METADATA_FUZZY_MATCH: True}, + entities={entity.name: entity for entity in entities}, + entities_list=entities, + response=response, + ) + + def _recognize_unknown_names( + self, + lang_intents: LanguageIntents, + user_input: ConversationInput, + slot_lists: dict[str, SlotList], + intent_context: dict[str, Any] | None, + ) -> RecognizeResult | None: + """Return result with unknown names for an error message.""" + maybe_result: RecognizeResult | None = None + + best_num_matched_entities = 0 + best_num_unmatched_entities = 0 + best_num_unmatched_ranges = 0 + for result in recognize_all( + user_input.text, + lang_intents.intents, + slot_lists=slot_lists, + intent_context=intent_context, + allow_unmatched_entities=True, + ): + if result.text_chunks_matched < 1: + # Skip results that don't match any literal text + continue + + # Don't count missing entities that couldn't be filled from context + num_matched_entities = 0 + for matched_entity in result.entities_list: + if matched_entity.name not in result.unmatched_entities: + num_matched_entities += 1 + + num_unmatched_entities = 0 + num_unmatched_ranges = 0 + for unmatched_entity in result.unmatched_entities_list: + if isinstance(unmatched_entity, UnmatchedTextEntity): + if unmatched_entity.text != MISSING_ENTITY: + num_unmatched_entities += 1 + elif isinstance(unmatched_entity, UnmatchedRangeEntity): + num_unmatched_ranges += 1 + num_unmatched_entities += 1 + else: + num_unmatched_entities += 1 + + if ( + (maybe_result is None) # first result + or ( + # More literal text matched + result.text_chunks_matched > maybe_result.text_chunks_matched + ) + or ( + # More entities matched + num_matched_entities > best_num_matched_entities + ) + or ( + # Fewer unmatched entities + (num_matched_entities == best_num_matched_entities) + and (num_unmatched_entities < best_num_unmatched_entities) + ) + or ( + # Prefer unmatched ranges + (num_matched_entities == best_num_matched_entities) + and (num_unmatched_entities == best_num_unmatched_entities) + and (num_unmatched_ranges > best_num_unmatched_ranges) + ) + or ( + # Prefer match failures with entities + (result.text_chunks_matched == maybe_result.text_chunks_matched) + and (num_unmatched_entities == best_num_unmatched_entities) + and (num_unmatched_ranges == best_num_unmatched_ranges) + and ( + ("name" in result.entities) + or ("name" in result.unmatched_entities) + ) + ) + ): + maybe_result = result + best_num_matched_entities = num_matched_entities + best_num_unmatched_entities = num_unmatched_entities + best_num_unmatched_ranges = num_unmatched_ranges + + return maybe_result + def _get_unexposed_entity_names(self, text: str) -> TextSlotList: """Get filtered slot list with unexposed entity names in Home Assistant.""" if self._unexposed_names_trie is None: @@ -851,7 +962,7 @@ class DefaultAgent(ConversationEntity): if lang_intents is None: return - self._make_slot_lists() + await self._make_slot_lists() async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None: """Load all intents of a language with lock.""" @@ -1002,12 +1113,85 @@ class DefaultAgent(ConversationEntity): intent_responses = responses_dict.get("intents", {}) error_responses = responses_dict.get("errors", {}) + if not self.fuzzy_matching: + _LOGGER.debug("Fuzzy matching is disabled") + return LanguageIntents( + intents, + intents_dict, + intent_responses, + error_responses, + language_variant, + ) + + # Load fuzzy + fuzzy_info = get_fuzzy_language(language_variant, json_load=json_load) + if fuzzy_info is None: + _LOGGER.debug( + "Fuzzy matching not available for language: %s", language_variant + ) + return LanguageIntents( + intents, + intents_dict, + intent_responses, + error_responses, + language_variant, + ) + + if self._fuzzy_config is None: + # Load shared config + self._fuzzy_config = get_fuzzy_config(json_load=json_load) + _LOGGER.debug("Loaded shared fuzzy matching config") + + assert self._fuzzy_config is not None + + fuzzy_matcher: FuzzyNgramMatcher | None = None + fuzzy_responses: FuzzyLanguageResponses | None = None + + start_time = time.monotonic() + fuzzy_responses = fuzzy_info.responses + fuzzy_matcher = FuzzyNgramMatcher( + intents=intents, + intent_models={ + intent_name: Sqlite3NgramModel( + order=fuzzy_model.order, + words={ + word: str(word_id) + for word, word_id in fuzzy_model.words.items() + }, + database_path=fuzzy_model.database_path, + ) + for intent_name, fuzzy_model in fuzzy_info.ngram_models.items() + }, + intent_slot_list_names=self._fuzzy_config.slot_list_names, + slot_combinations={ + intent_name: { + combo_key: [ + SlotCombinationInfo( + name_domains=(set(name_domains) if name_domains else None) + ) + ] + for combo_key, name_domains in intent_combos.items() + } + for intent_name, intent_combos in self._fuzzy_config.slot_combinations.items() + }, + domain_keywords=fuzzy_info.domain_keywords, + stop_words=fuzzy_info.stop_words, + ) + _LOGGER.debug( + "Loaded fuzzy matcher in %s second(s): language=%s, intents=%s", + time.monotonic() - start_time, + language_variant, + sorted(fuzzy_matcher.intent_models.keys()), + ) + return LanguageIntents( intents, intents_dict, intent_responses, error_responses, language_variant, + fuzzy_matcher=fuzzy_matcher, + fuzzy_responses=fuzzy_responses, ) @core.callback @@ -1027,8 +1211,7 @@ class DefaultAgent(ConversationEntity): # Slot lists have changed, so we must clear the cache self._intent_cache.clear() - @core.callback - def _make_slot_lists(self) -> dict[str, SlotList]: + async def _make_slot_lists(self) -> dict[str, SlotList]: """Create slot lists with areas and entity names/aliases.""" if self._slot_lists is not None: return self._slot_lists @@ -1089,6 +1272,10 @@ class DefaultAgent(ConversationEntity): "floor": TextSlotList.from_tuples(floor_names, allow_template=False), } + # Reload fuzzy matchers with new slot lists + if self.fuzzy_matching: + await self.hass.async_add_executor_job(self._load_fuzzy_matchers) + self._listen_clear_slot_list() _LOGGER.debug( @@ -1098,6 +1285,25 @@ class DefaultAgent(ConversationEntity): return self._slot_lists + def _load_fuzzy_matchers(self) -> None: + """Reload fuzzy matchers for all loaded languages.""" + for lang_intents in self._lang_intents.values(): + if (not isinstance(lang_intents, LanguageIntents)) or ( + lang_intents.fuzzy_matcher is None + ): + continue + + lang_matcher = lang_intents.fuzzy_matcher + lang_intents.fuzzy_matcher = FuzzyNgramMatcher( + intents=lang_matcher.intents, + intent_models=lang_matcher.intent_models, + intent_slot_list_names=lang_matcher.intent_slot_list_names, + slot_combinations=lang_matcher.slot_combinations, + domain_keywords=lang_matcher.domain_keywords, + stop_words=lang_matcher.stop_words, + slot_lists=self._slot_lists, + ) + def _make_intent_context( self, user_input: ConversationInput ) -> dict[str, Any] | None: @@ -1183,7 +1389,7 @@ class DefaultAgent(ConversationEntity): for trigger_intent in trigger_intents.intents.values(): for intent_data in trigger_intent.data: for sentence in intent_data.sentences: - _collect_list_references(sentence, wildcard_names) + _collect_list_references(sentence.expression, wildcard_names) for wildcard_name in wildcard_names: trigger_intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name) @@ -1520,11 +1726,9 @@ def _get_match_error_response( def _collect_list_references(expression: Expression, list_names: set[str]) -> None: """Collect list reference names recursively.""" - if isinstance(expression, Sequence): - seq: Sequence = expression - for item in seq.items: + if isinstance(expression, Group): + for item in expression.items: _collect_list_references(item, list_names) elif isinstance(expression, ListReference): # {list} - list_ref: ListReference = expression - list_names.add(list_ref.slot_name) + list_names.add(expression.slot_name) diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index efcdcb8d69b..290e3aab955 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -26,7 +26,11 @@ from .agent_manager import ( get_agent_manager, ) from .const import DATA_COMPONENT, DATA_DEFAULT_ENTITY -from .default_agent import METADATA_CUSTOM_FILE, METADATA_CUSTOM_SENTENCE +from .default_agent import ( + METADATA_CUSTOM_FILE, + METADATA_CUSTOM_SENTENCE, + METADATA_FUZZY_MATCH, +) from .entity import ConversationEntity from .models import ConversationInput @@ -240,6 +244,8 @@ async def websocket_hass_agent_debug( "sentence_template": "", # When match is incomplete, this will contain the best slot guesses "unmatched_slots": _get_unmatched_slots(intent_result), + # True if match was not exact + "fuzzy_match": False, } if successful_match: @@ -251,16 +257,19 @@ async def websocket_hass_agent_debug( if intent_result.intent_sentence is not None: result_dict["sentence_template"] = intent_result.intent_sentence.text - # Inspect metadata to determine if this matched a custom sentence - if intent_result.intent_metadata and intent_result.intent_metadata.get( - METADATA_CUSTOM_SENTENCE - ): - result_dict["source"] = "custom" - result_dict["file"] = intent_result.intent_metadata.get( - METADATA_CUSTOM_FILE + if intent_result.intent_metadata: + # Inspect metadata to determine if this matched a custom sentence + if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE): + result_dict["source"] = "custom" + result_dict["file"] = intent_result.intent_metadata.get( + METADATA_CUSTOM_FILE + ) + else: + result_dict["source"] = "builtin" + + result_dict["fuzzy_match"] = intent_result.intent_metadata.get( + METADATA_FUZZY_MATCH, False ) - else: - result_dict["source"] = "builtin" result_dicts.append(result_dict) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 31adffad064..e7d096212ba 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"] + "requirements": ["hassil==3.1.0", "home-assistant-intents==2025.7.30"] } diff --git a/homeassistant/components/cookidoo/manifest.json b/homeassistant/components/cookidoo/manifest.json index 5264e47a709..b4cf653f810 100644 --- a/homeassistant/components/cookidoo/manifest.json +++ b/homeassistant/components/cookidoo/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["cookidoo_api"], "quality_scale": "silver", - "requirements": ["cookidoo-api==0.12.2"] + "requirements": ["cookidoo-api==0.14.0"] } diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 947fe514747..799ff378a35 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.15.0"], + "requirements": ["pydaikin==2.16.0"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index ad7ddcba285..0c001921c7a 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import datetime from typing import Any +from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseMedia, MediaClass, @@ -396,6 +397,15 @@ class DemoBrowsePlayer(AbstractDemoPlayer): _attr_supported_features = BROWSE_PLAYER_SUPPORT + async def async_browse_media( + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + + return await media_source.async_browse_media(self.hass, media_content_id) + class DemoGroupPlayer(AbstractDemoPlayer): """A Demo media player that supports grouping.""" diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 8bdf448bfba..3d4c62ee1c7 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -99,6 +99,18 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry, version=1, minor_version=3 ) + if config_entry.minor_version < 4: + # Ensure we use the correct units + new_options = {**config_entry.options} + + if new_options.get("unit_prefix") == "\u00b5": + # Ensure we use the preferred coding of μ + new_options["unit_prefix"] = "\u03bc" + + hass.config_entries.async_update_entry( + config_entry, options=new_options, version=1, minor_version=4 + ) + _LOGGER.debug( "Migration to configuration version %s.%s successful", config_entry.version, diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index b5dee1deee3..be371837442 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -36,7 +36,7 @@ from .const import ( UNIT_PREFIXES = [ selector.SelectOptionDict(value="n", label="n (nano)"), - selector.SelectOptionDict(value="µ", label="µ (micro)"), + selector.SelectOptionDict(value="μ", label="μ (micro)"), selector.SelectOptionDict(value="m", label="m (milli)"), selector.SelectOptionDict(value="k", label="k (kilo)"), selector.SelectOptionDict(value="M", label="M (mega)"), @@ -142,7 +142,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): options_flow = OPTIONS_FLOW VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index da35975c193..68ee5739ab7 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -63,7 +63,7 @@ ATTR_SOURCE_ID = "source" UNIT_PREFIXES = { None: 1, "n": 1e-9, - "µ": 1e-6, + "μ": 1e-6, "m": 1e-3, "k": 1e3, "M": 1e6, diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 599e5ecae5b..32abe0684f7 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==1.2.0", + "aiodhcpwatcher==1.2.1", "aiodiscover==2.7.1", "cached-ipaddress==0.10.0" ] diff --git a/homeassistant/components/dremel_3d_printer/entity.py b/homeassistant/components/dremel_3d_printer/entity.py index 46686e47e1f..c2823e594a8 100644 --- a/homeassistant/components/dremel_3d_printer/entity.py +++ b/homeassistant/components/dremel_3d_printer/entity.py @@ -30,6 +30,7 @@ class Dremel3DPrinterEntity(CoordinatorEntity[Dremel3DPrinterDataUpdateCoordinat """Return device information about this Dremel printer.""" return DeviceInfo( identifiers={(DOMAIN, self._api.get_serial_number())}, + serial_number=self._api.get_serial_number(), manufacturer=self._api.get_manufacturer(), model=self._api.get_model(), name=self._api.get_title(), diff --git a/homeassistant/components/emoncms/const.py b/homeassistant/components/emoncms/const.py index a3b4629493f..329ec9e3a12 100644 --- a/homeassistant/components/emoncms/const.py +++ b/homeassistant/components/emoncms/const.py @@ -2,7 +2,6 @@ import logging -CONF_EXCLUDE_FEEDID = "exclude_feed_id" CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id" CONF_MESSAGE = "message" CONF_SUCCESS = "success" diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index c5a25104549..2ca4e28a36d 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -34,13 +34,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .config_flow import sensor_name -from .const import ( - CONF_EXCLUDE_FEEDID, - CONF_ONLY_INCLUDE_FEEDID, - FEED_ID, - FEED_NAME, - FEED_TAG, -) +from .const import CONF_ONLY_INCLUDE_FEEDID, FEED_ID, FEED_NAME, FEED_TAG from .coordinator import EmonCMSConfigEntry, EmoncmsCoordinator SENSORS: dict[str | None, SensorEntityDescription] = { @@ -163,7 +157,7 @@ SENSORS: dict[str | None, SensorEntityDescription] = { native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, ), - "µg/m³": SensorEntityDescription( + "μg/m³": SensorEntityDescription( key="concentration|microgram_per_cubic_meter", translation_key="concentration", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -200,12 +194,11 @@ async def async_setup_entry( ) -> None: """Set up the emoncms sensors.""" name = sensor_name(entry.data[CONF_URL]) - exclude_feeds = entry.data.get(CONF_EXCLUDE_FEEDID) include_only_feeds = entry.options.get( CONF_ONLY_INCLUDE_FEEDID, entry.data.get(CONF_ONLY_INCLUDE_FEEDID) ) - if exclude_feeds is None and include_only_feeds is None: + if include_only_feeds is None: return coordinator = entry.runtime_data diff --git a/homeassistant/components/emoncms/strings.json b/homeassistant/components/emoncms/strings.json index f68ea92d26c..e41a7e8bd03 100644 --- a/homeassistant/components/emoncms/strings.json +++ b/homeassistant/components/emoncms/strings.json @@ -111,14 +111,6 @@ } }, "issues": { - "remove_value_template": { - "title": "The {domain} integration cannot start", - "description": "Configuring {domain} using YAML is being removed and the `{parameter}` parameter cannot be imported.\n\nPlease remove `{parameter}` from your `{domain}` yaml configuration and restart Home Assistant\n\nAlternatively, you may entirely remove the `{domain}` configuration from your configuration.yaml, restart Home Assistant, and add the {domain} integration manually." - }, - "missing_include_only_feed_id": { - "title": "No feed synchronized with the {domain} sensor", - "description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration." - }, "migrate_database": { "title": "Upgrade your emoncms version", "description": "Your [emoncms]({url}) does not ship a unique identifier.\n\nPlease upgrade to at least version 11.5.7 and migrate your emoncms database.\n\nMore info in the [emoncms documentation]({doc_url})" diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 2ab00d6ca42..5394a797272 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,10 +1,11 @@ """Support for sending data to Emoncms.""" -from datetime import timedelta -from http import HTTPStatus +from datetime import datetime, timedelta +from functools import partial import logging -import requests +import aiohttp +from pyemoncms import EmoncmsClient import voluptuous as vol from homeassistant.const import ( @@ -17,9 +18,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, state as state_helper -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -42,61 +43,51 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_send_to_emoncms( + hass: HomeAssistant, + emoncms_client: EmoncmsClient, + whitelist: list[str], + node: str | int, + _: datetime, +) -> None: + """Send data to Emoncms.""" + payload_dict = {} + + for entity_id in whitelist: + state = hass.states.get(entity_id) + if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE): + continue + try: + payload_dict[entity_id] = state_helper.state_as_number(state) + except ValueError: + continue + + if payload_dict: + try: + await emoncms_client.async_input_post(data=payload_dict, node=node) + except (aiohttp.ClientError, TimeoutError) as err: + _LOGGER.warning("Network error when sending data to Emoncms: %s", err) + except ValueError as err: + _LOGGER.warning("Value error when preparing data for Emoncms: %s", err) + else: + _LOGGER.debug("Sent data to Emoncms: %s", payload_dict) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Emoncms history component.""" conf = config[DOMAIN] whitelist = conf.get(CONF_WHITELIST) + input_node = str(conf.get(CONF_INPUTNODE)) - def send_data(url, apikey, node, payload): - """Send payload data to Emoncms.""" - try: - fullurl = f"{url}/input/post.json" - data = {"apikey": apikey, "data": payload} - parameters = {"node": node} - req = requests.post( - fullurl, params=parameters, data=data, allow_redirects=True, timeout=5 - ) + emoncms_client = EmoncmsClient( + url=conf.get(CONF_URL), + api_key=conf.get(CONF_API_KEY), + session=async_get_clientsession(hass), + ) + async_track_time_interval( + hass, + partial(async_send_to_emoncms, hass, emoncms_client, whitelist, input_node), + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)), + ) - except requests.exceptions.RequestException: - _LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl) - - else: - if req.status_code != HTTPStatus.OK: - _LOGGER.error( - "Error saving data %s to %s (http status code = %d)", - payload, - fullurl, - req.status_code, - ) - - def update_emoncms(time): - """Send whitelisted entities states regularly to Emoncms.""" - payload_dict = {} - - for entity_id in whitelist: - state = hass.states.get(entity_id) - - if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE): - continue - - try: - payload_dict[entity_id] = state_helper.state_as_number(state) - except ValueError: - continue - - if payload_dict: - payload = ",".join(f"{key}:{val}" for key, val in payload_dict.items()) - - send_data( - conf.get(CONF_URL), - conf.get(CONF_API_KEY), - str(conf.get(CONF_INPUTNODE)), - f"{{{payload}}}", - ) - - track_point_in_time( - hass, update_emoncms, time + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)) - ) - - update_emoncms(dt_util.utcnow()) return True diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index e73f76f7528..3c8c445b766 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -1,8 +1,9 @@ { "domain": "emoncms_history", "name": "Emoncms History", - "codeowners": [], + "codeowners": ["@alexandrecuer"], "documentation": "https://www.home-assistant.io/integrations/emoncms_history", "iot_class": "local_polling", - "quality_scale": "legacy" + "quality_scale": "legacy", + "requirements": ["pyemoncms==0.1.2"] } diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index be9e2ecb4cc..3e2f6dcbc8f 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -93,6 +93,7 @@ class EmonitorPowerSensor(CoordinatorEntity[EmonitorStatus], SensorEntity): manufacturer="Powerhouse Dynamics, Inc.", name=device_name, sw_version=emonitor_status.hardware.firmware_version, + serial_number=emonitor_status.hardware.serial_number, ) self._attr_extra_state_attributes = {"channel": channel_number} self._attr_native_value = self._paired_attr(self.entity_description.key) diff --git a/homeassistant/components/enigma2/coordinator.py b/homeassistant/components/enigma2/coordinator.py index 9710d7f547f..02e50c2cc06 100644 --- a/homeassistant/components/enigma2/coordinator.py +++ b/homeassistant/components/enigma2/coordinator.py @@ -1,5 +1,6 @@ """Data update coordinator for the Enigma2 integration.""" +import asyncio import logging from openwebif.api import OpenWebIfDevice, OpenWebIfStatus @@ -30,6 +31,8 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN LOGGER = logging.getLogger(__package__) +SETUP_TIMEOUT = 10 + type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator] @@ -79,7 +82,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]): async def _async_setup(self) -> None: """Provide needed data to the device info.""" - about = await self.device.get_about() + about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT) self.device.mac_address = about["info"]["ifaces"][0]["mac"] self.device_info["model"] = about["info"]["model"] self.device_info["manufacturer"] = about["info"]["brand"] diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index e95ab1179e1..62d276b4224 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pyenphase import Envoy from homeassistant.const import CONF_HOST @@ -42,6 +44,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b }, ) + # register envoy before via_device is used + device_registry = dr.async_get(hass) + if TYPE_CHECKING: + assert envoy.serial_number + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, envoy.serial_number)}, + manufacturer="Enphase", + name=coordinator.name, + model=envoy.envoy_model, + sw_version=str(envoy.firmware), + hw_version=envoy.part_number, + serial_number=envoy.serial_number, + ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 2628406f56f..5dcc2f28c7f 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from operator import attrgetter -from pyenphase import EnvoyEncharge, EnvoyEnpower +from pyenphase import EnvoyC6CC, EnvoyCollar, EnvoyEncharge, EnvoyEnpower from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -72,6 +72,42 @@ ENPOWER_SENSORS = ( ) +@dataclass(frozen=True, kw_only=True) +class EnvoyCollarBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes an Envoy IQ Meter Collar binary sensor entity.""" + + value_fn: Callable[[EnvoyCollar], bool] + + +COLLAR_SENSORS = ( + EnvoyCollarBinarySensorEntityDescription( + key="communicating", + translation_key="communicating", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=attrgetter("communicating"), + ), +) + + +@dataclass(frozen=True, kw_only=True) +class EnvoyC6CCBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes an C6 Combiner controller binary sensor entity.""" + + value_fn: Callable[[EnvoyC6CC], bool] + + +C6CC_SENSORS = ( + EnvoyC6CCBinarySensorEntityDescription( + key="communicating", + translation_key="communicating", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=attrgetter("communicating"), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: EnphaseConfigEntry, @@ -95,6 +131,18 @@ async def async_setup_entry( for description in ENPOWER_SENSORS ) + if envoy_data.collar: + entities.extend( + EnvoyCollarBinarySensorEntity(coordinator, description) + for description in COLLAR_SENSORS + ) + + if envoy_data.c6cc: + entities.extend( + EnvoyC6CCBinarySensorEntity(coordinator, description) + for description in C6CC_SENSORS + ) + async_add_entities(entities) @@ -168,3 +216,69 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity): enpower = self.data.enpower assert enpower is not None return self.entity_description.value_fn(enpower) + + +class EnvoyCollarBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an IQ Meter Collar binary_sensor entity.""" + + entity_description: EnvoyCollarBinarySensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyCollarBinarySensorEntityDescription, + ) -> None: + """Init the Collar base entity.""" + super().__init__(coordinator, description) + collar_data = self.data.collar + assert collar_data is not None + self._attr_unique_id = f"{collar_data.serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, collar_data.serial_number)}, + manufacturer="Enphase", + model="IQ Meter Collar", + name=f"Collar {collar_data.serial_number}", + sw_version=str(collar_data.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + serial_number=collar_data.serial_number, + ) + + @property + def is_on(self) -> bool: + """Return the state of the Collar binary_sensor.""" + collar_data = self.data.collar + assert collar_data is not None + return self.entity_description.value_fn(collar_data) + + +class EnvoyC6CCBinarySensorEntity(EnvoyBaseBinarySensorEntity): + """Defines an C6 Combiner binary_sensor entity.""" + + entity_description: EnvoyC6CCBinarySensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyC6CCBinarySensorEntityDescription, + ) -> None: + """Init the C6 Combiner base entity.""" + super().__init__(coordinator, description) + c6cc_data = self.data.c6cc + assert c6cc_data is not None + self._attr_unique_id = f"{c6cc_data.serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, c6cc_data.serial_number)}, + manufacturer="Enphase", + model="C6 COMBINER CONTROLLER", + name=f"C6 Combiner {c6cc_data.serial_number}", + sw_version=str(c6cc_data.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + serial_number=c6cc_data.serial_number, + ) + + @property + def is_on(self) -> bool: + """Return the state of the C6 Combiner binary_sensor.""" + c6cc_data = self.data.c6cc + assert c6cc_data is not None + return self.entity_description.value_fn(c6cc_data) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index e337dac74e0..0e1e89cf1e3 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -1,13 +1,13 @@ { "domain": "enphase_envoy", "name": "Enphase Envoy", - "codeowners": ["@bdraco", "@cgarwood", "@joostlek", "@catsmanac"], + "codeowners": ["@bdraco", "@cgarwood", "@catsmanac"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==2.2.3"], + "requirements": ["pyenphase==2.3.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 63a2a09a6f5..e771233b069 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -12,6 +12,8 @@ from typing import TYPE_CHECKING from pyenphase import ( EnvoyACBPower, EnvoyBatteryAggregate, + EnvoyC6CC, + EnvoyCollar, EnvoyEncharge, EnvoyEnchargeAggregate, EnvoyEnchargePower, @@ -790,6 +792,58 @@ ENPOWER_SENSORS = ( ) +@dataclass(frozen=True, kw_only=True) +class EnvoyCollarSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy Collar sensor entity.""" + + value_fn: Callable[[EnvoyCollar], datetime.datetime | int | float | str] + + +COLLAR_SENSORS = ( + EnvoyCollarSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=attrgetter("temperature"), + ), + EnvoyCollarSensorEntityDescription( + key=LAST_REPORTED_KEY, + translation_key=LAST_REPORTED_KEY, + native_unit_of_measurement=None, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda collar: dt_util.utc_from_timestamp(collar.last_report_date), + ), + EnvoyCollarSensorEntityDescription( + key="grid_state", + translation_key="grid_status", + value_fn=lambda collar: collar.grid_state, + ), + EnvoyCollarSensorEntityDescription( + key="mid_state", + translation_key="mid_state", + value_fn=lambda collar: collar.mid_state, + ), +) + + +@dataclass(frozen=True, kw_only=True) +class EnvoyC6CCSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy C6 Combiner controller sensor entity.""" + + value_fn: Callable[[EnvoyC6CC], datetime.datetime] + + +C6CC_SENSORS = ( + EnvoyC6CCSensorEntityDescription( + key=LAST_REPORTED_KEY, + translation_key=LAST_REPORTED_KEY, + native_unit_of_measurement=None, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda c6cc: dt_util.utc_from_timestamp(c6cc.last_report_date), + ), +) + + @dataclass(frozen=True) class EnvoyEnchargeAggregateRequiredKeysMixin: """Mixin for required keys.""" @@ -1050,6 +1104,15 @@ async def async_setup_entry( AggregateBatteryEntity(coordinator, description) for description in AGGREGATE_BATTERY_SENSORS ) + if envoy_data.collar: + entities.extend( + EnvoyCollarEntity(coordinator, description) + for description in COLLAR_SENSORS + ) + if envoy_data.c6cc: + entities.extend( + EnvoyC6CCEntity(coordinator, description) for description in C6CC_SENSORS + ) async_add_entities(entities) @@ -1488,3 +1551,70 @@ class AggregateBatteryEntity(EnvoySystemSensorEntity): battery_aggregate = self.data.battery_aggregate assert battery_aggregate is not None return self.entity_description.value_fn(battery_aggregate) + + +class EnvoyCollarEntity(EnvoySensorBaseEntity): + """Envoy Collar sensor entity.""" + + entity_description: EnvoyCollarSensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyCollarSensorEntityDescription, + ) -> None: + """Initialize Collar entity.""" + super().__init__(coordinator, description) + collar_data = self.data.collar + assert collar_data is not None + self._serial_number = collar_data.serial_number + self._attr_unique_id = f"{collar_data.serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, collar_data.serial_number)}, + manufacturer="Enphase", + model="IQ Meter Collar", + name=f"Collar {collar_data.serial_number}", + sw_version=str(collar_data.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + serial_number=collar_data.serial_number, + ) + + @property + def native_value(self) -> datetime.datetime | int | float | str: + """Return the state of the collar sensors.""" + collar_data = self.data.collar + assert collar_data is not None + return self.entity_description.value_fn(collar_data) + + +class EnvoyC6CCEntity(EnvoySensorBaseEntity): + """Envoy C6CC sensor entity.""" + + entity_description: EnvoyC6CCSensorEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyC6CCSensorEntityDescription, + ) -> None: + """Initialize Encharge entity.""" + super().__init__(coordinator, description) + c6cc_data = self.data.c6cc + assert c6cc_data is not None + self._attr_unique_id = f"{c6cc_data.serial_number}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, c6cc_data.serial_number)}, + manufacturer="Enphase", + model="C6 COMBINER CONTROLLER", + name=f"C6 Combiner {c6cc_data.serial_number}", + sw_version=str(c6cc_data.firmware_version), + via_device=(DOMAIN, self.envoy_serial_num), + serial_number=c6cc_data.serial_number, + ) + + @property + def native_value(self) -> datetime.datetime: + """Return the state of the c6cc inventory sensors.""" + c6cc_data = self.data.c6cc + assert c6cc_data is not None + return self.entity_description.value_fn(c6cc_data) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index ffe0ccb1271..17ed8eff67e 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -407,6 +407,12 @@ }, "last_report_duration": { "name": "Last report duration" + }, + "grid_status": { + "name": "[%key:component::enphase_envoy::entity::binary_sensor::grid_status::name%]" + }, + "mid_state": { + "name": "MID state" } }, "switch": { diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index 2c9bee32734..385c88d6eb9 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False DEFAULT_PORT: Final = 6053 -STABLE_BLE_VERSION_STR = "2025.5.0" +STABLE_BLE_VERSION_STR = "2025.8.0" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 4d5de77b1e0..74b429cdfa1 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import base64 from functools import partial import logging @@ -15,7 +14,6 @@ from aioesphomeapi import ( APIVersion, DeviceInfo as EsphomeDeviceInfo, EncryptionPlaintextAPIError, - EntityInfo, HomeassistantServiceCall, InvalidAuthAPIError, InvalidEncryptionKeyAPIError, @@ -63,7 +61,6 @@ from homeassistant.helpers.issue_registry import ( ) from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template -from homeassistant.util.async_ import create_eager_task from .bluetooth import async_connect_scanner from .const import ( @@ -425,14 +422,7 @@ class ESPHomeManager: unique_id_is_mac_address = unique_id and ":" in unique_id if entry.options.get(CONF_SUBSCRIBE_LOGS): self._async_subscribe_logs(self._async_get_equivalent_log_level()) - results = await asyncio.gather( - create_eager_task(cli.device_info()), - create_eager_task(cli.list_entities_services()), - ) - - device_info: EsphomeDeviceInfo = results[0] - entity_infos_services: tuple[list[EntityInfo], list[UserService]] = results[1] - entity_infos, services = entity_infos_services + device_info, entity_infos, services = await cli.device_info_and_list_entities() device_mac = format_mac(device_info.mac_address) mac_address_matches = unique_id == device_mac @@ -564,11 +554,11 @@ class ESPHomeManager: ) entry_data.loaded_platforms.add(Platform.ASSIST_SATELLITE) - cli.subscribe_states(entry_data.async_update_state) - cli.subscribe_service_calls(self.async_on_service_call) - cli.subscribe_home_assistant_states( - self.async_on_state_subscription, - self.async_on_state_request, + cli.subscribe_home_assistant_states_and_services( + on_state=entry_data.async_update_state, + on_service_call=self.async_on_service_call, + on_state_sub=self.async_on_state_subscription, + on_state_request=self.async_on_state_request, ) entry_data.async_save_to_store() diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 6bf164aa9bc..ffb02571742 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==37.2.2", + "aioesphomeapi==39.0.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.1.0" ], diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 2d43d40bfb3..a35d93c9fe1 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -10,6 +10,7 @@ from urllib.parse import urlparse from aioesphomeapi import ( EntityInfo, MediaPlayerCommand, + MediaPlayerEntityFeature as EspMediaPlayerEntityFeature, MediaPlayerEntityState, MediaPlayerFormatPurpose, MediaPlayerInfo, @@ -50,9 +51,36 @@ _STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumM EspMediaPlayerState.IDLE: MediaPlayerState.IDLE, EspMediaPlayerState.PLAYING: MediaPlayerState.PLAYING, EspMediaPlayerState.PAUSED: MediaPlayerState.PAUSED, + EspMediaPlayerState.OFF: MediaPlayerState.OFF, + EspMediaPlayerState.ON: MediaPlayerState.ON, } ) +_FEATURES = { + EspMediaPlayerEntityFeature.PAUSE: MediaPlayerEntityFeature.PAUSE, + EspMediaPlayerEntityFeature.SEEK: MediaPlayerEntityFeature.SEEK, + EspMediaPlayerEntityFeature.VOLUME_SET: MediaPlayerEntityFeature.VOLUME_SET, + EspMediaPlayerEntityFeature.VOLUME_MUTE: MediaPlayerEntityFeature.VOLUME_MUTE, + EspMediaPlayerEntityFeature.PREVIOUS_TRACK: MediaPlayerEntityFeature.PREVIOUS_TRACK, + EspMediaPlayerEntityFeature.NEXT_TRACK: MediaPlayerEntityFeature.NEXT_TRACK, + EspMediaPlayerEntityFeature.TURN_ON: MediaPlayerEntityFeature.TURN_ON, + EspMediaPlayerEntityFeature.TURN_OFF: MediaPlayerEntityFeature.TURN_OFF, + EspMediaPlayerEntityFeature.PLAY_MEDIA: MediaPlayerEntityFeature.PLAY_MEDIA, + EspMediaPlayerEntityFeature.VOLUME_STEP: MediaPlayerEntityFeature.VOLUME_STEP, + EspMediaPlayerEntityFeature.SELECT_SOURCE: MediaPlayerEntityFeature.SELECT_SOURCE, + EspMediaPlayerEntityFeature.STOP: MediaPlayerEntityFeature.STOP, + EspMediaPlayerEntityFeature.CLEAR_PLAYLIST: MediaPlayerEntityFeature.CLEAR_PLAYLIST, + EspMediaPlayerEntityFeature.PLAY: MediaPlayerEntityFeature.PLAY, + EspMediaPlayerEntityFeature.SHUFFLE_SET: MediaPlayerEntityFeature.SHUFFLE_SET, + EspMediaPlayerEntityFeature.SELECT_SOUND_MODE: MediaPlayerEntityFeature.SELECT_SOUND_MODE, + EspMediaPlayerEntityFeature.BROWSE_MEDIA: MediaPlayerEntityFeature.BROWSE_MEDIA, + EspMediaPlayerEntityFeature.REPEAT_SET: MediaPlayerEntityFeature.REPEAT_SET, + EspMediaPlayerEntityFeature.GROUPING: MediaPlayerEntityFeature.GROUPING, + EspMediaPlayerEntityFeature.MEDIA_ANNOUNCE: MediaPlayerEntityFeature.MEDIA_ANNOUNCE, + EspMediaPlayerEntityFeature.MEDIA_ENQUEUE: MediaPlayerEntityFeature.MEDIA_ENQUEUE, + EspMediaPlayerEntityFeature.SEARCH_MEDIA: MediaPlayerEntityFeature.SEARCH_MEDIA, +} + ATTR_BYPASS_PROXY = "bypass_proxy" @@ -67,16 +95,12 @@ class EsphomeMediaPlayer( def _on_static_info_update(self, static_info: EntityInfo) -> None: """Set attrs from static info.""" super()._on_static_info_update(static_info) - flags = ( - MediaPlayerEntityFeature.PLAY_MEDIA - | MediaPlayerEntityFeature.BROWSE_MEDIA - | MediaPlayerEntityFeature.STOP - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.VOLUME_MUTE - | MediaPlayerEntityFeature.MEDIA_ANNOUNCE + esp_flags = EspMediaPlayerEntityFeature( + self._static_info.feature_flags_compat(self._api_version) ) - if self._static_info.supports_pause: - flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY + flags = MediaPlayerEntityFeature(0) + for espflag in esp_flags: + flags |= _FEATURES[espflag] self._attr_supported_features = flags self._entry_data.media_player_formats[self.unique_id] = cast( MediaPlayerInfo, static_info @@ -257,6 +281,24 @@ class EsphomeMediaPlayer( device_id=self._static_info.device_id, ) + @convert_api_error_ha_error + async def async_turn_on(self) -> None: + """Send turn on command.""" + self._client.media_player_command( + self._key, + command=MediaPlayerCommand.TURN_ON, + device_id=self._static_info.device_id, + ) + + @convert_api_error_ha_error + async def async_turn_off(self) -> None: + """Send turn off command.""" + self._client.media_player_command( + self._key, + command=MediaPlayerCommand.TURN_OFF, + device_id=self._static_info.device_id, + ) + def _is_url(url: str) -> bool: """Validate the URL can be parsed and at least has scheme + netloc.""" diff --git a/homeassistant/components/fan/intent.py b/homeassistant/components/fan/intent.py new file mode 100644 index 00000000000..ef088a4bba9 --- /dev/null +++ b/homeassistant/components/fan/intent.py @@ -0,0 +1,31 @@ +"""Intents for the fan integration.""" + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import ATTR_PERCENTAGE, DOMAIN, SERVICE_TURN_ON + +INTENT_FAN_SET_SPEED = "HassFanSetSpeed" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the fan intents.""" + intent.async_register( + hass, + intent.ServiceIntentHandler( + INTENT_FAN_SET_SPEED, + DOMAIN, + SERVICE_TURN_ON, + description="Sets a fan's speed by percentage", + required_domains={DOMAIN}, + platforms={DOMAIN}, + required_slots={ + ATTR_PERCENTAGE: intent.IntentSlotInfo( + description="The speed percentage of the fan", + value_schema=vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), + ) + }, + ), + ) diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 222a7e44a45..099123ccd9b 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -30,7 +30,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bo verbose=False, ) coordinator = FoscamCoordinator(hass, entry, session) - await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator @@ -89,7 +88,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> async def async_migrate_entities(hass: HomeAssistant, entry: FoscamConfigEntry) -> None: - """Migrate old entry.""" + """Migrate old entries to support config_entry_id-based unique IDs.""" @callback def _update_unique_id( diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py index 562c3f42f8b..93ec5f909c4 100644 --- a/homeassistant/components/foscam/config_flow.py +++ b/homeassistant/components/foscam/config_flow.py @@ -26,7 +26,7 @@ from .const import CONF_RTSP_PORT, CONF_STREAM, DOMAIN, LOGGER STREAMS = ["Main", "Sub"] DEFAULT_PORT = 88 -DEFAULT_RTSP_PORT = 554 +DEFAULT_RTSP_PORT = 88 DATA_SCHEMA = vol.Schema( diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py index 38088cf3f6f..33c1b31aeec 100644 --- a/homeassistant/components/foscam/const.py +++ b/homeassistant/components/foscam/const.py @@ -11,3 +11,16 @@ CONF_STREAM = "stream" SERVICE_PTZ = "ptz" SERVICE_PTZ_PRESET = "ptz_preset" + +SUPPORTED_SWITCHES = [ + "flip_switch", + "mirror_switch", + "ir_switch", + "sleep_switch", + "white_light_switch", + "siren_alarm_switch", + "turn_off_volume_switch", + "light_status_switch", + "hdr_switch", + "wdr_switch", +] diff --git a/homeassistant/components/foscam/coordinator.py b/homeassistant/components/foscam/coordinator.py index 72bf60cffe0..50ddd76ddb3 100644 --- a/homeassistant/components/foscam/coordinator.py +++ b/homeassistant/components/foscam/coordinator.py @@ -1,8 +1,8 @@ """The foscam coordinator object.""" import asyncio +from dataclasses import dataclass from datetime import timedelta -from typing import Any from libpyfoscamcgi import FoscamCamera @@ -15,9 +15,35 @@ from .const import DOMAIN, LOGGER type FoscamConfigEntry = ConfigEntry[FoscamCoordinator] -class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]): +@dataclass +class FoscamDeviceInfo: + """A data class representing the current state and configuration of a Foscam camera device.""" + + dev_info: dict + product_info: dict + + is_open_ir: bool + is_flip: bool + is_mirror: bool + + is_asleep: dict + is_open_white_light: bool + is_siren_alarm: bool + + volume: int + speak_volume: int + is_turn_off_volume: bool + is_turn_off_light: bool + + is_open_wdr: bool | None = None + is_open_hdr: bool | None = None + + +class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]): """Foscam coordinator.""" + config_entry: FoscamConfigEntry + def __init__( self, hass: HomeAssistant, @@ -34,24 +60,82 @@ class FoscamCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) self.session = session - async def _async_update_data(self) -> dict[str, Any]: + def gather_all_configs(self) -> FoscamDeviceInfo: + """Get all Foscam configurations.""" + ret_dev_info, dev_info = self.session.get_dev_info() + dev_info = dev_info if ret_dev_info == 0 else {} + + ret_product_info, product_info = self.session.get_product_all_info() + product_info = product_info if ret_product_info == 0 else {} + + ret_ir, infra_led_config = self.session.get_infra_led_config() + is_open_ir = infra_led_config["mode"] == "1" if ret_ir == 0 else False + + ret_mf, mirror_flip_setting = self.session.get_mirror_and_flip_setting() + is_flip = mirror_flip_setting["isFlip"] == "1" if ret_mf == 0 else False + is_mirror = mirror_flip_setting["isMirror"] == "1" if ret_mf == 0 else False + + ret_sleep, sleep_setting = self.session.is_asleep() + is_asleep = {"supported": ret_sleep == 0, "status": bool(int(sleep_setting))} + + ret_wl, is_open_white_light = self.session.getWhiteLightBrightness() + is_open_white_light_val = ( + is_open_white_light["enable"] == "1" if ret_wl == 0 else False + ) + + ret_sc, is_siren_alarm = self.session.getSirenConfig() + is_siren_alarm_val = ( + is_siren_alarm["sirenEnable"] == "1" if ret_sc == 0 else False + ) + + ret_vol, volume = self.session.getAudioVolume() + volume_val = int(volume["volume"]) if ret_vol == 0 else 0 + + ret_sv, speak_volume = self.session.getSpeakVolume() + speak_volume_val = int(speak_volume["SpeakVolume"]) if ret_sv == 0 else 0 + + ret_ves, is_turn_off_volume = self.session.getVoiceEnableState() + is_turn_off_volume_val = not ( + ret_ves == 0 and is_turn_off_volume["isEnable"] == "1" + ) + + ret_les, is_turn_off_light = self.session.getLedEnableState() + is_turn_off_light_val = not ( + ret_les == 0 and is_turn_off_light["isEnable"] == "0" + ) + + is_open_wdr = None + is_open_hdr = None + reserve3 = product_info.get("reserve3") + reserve3_int = int(reserve3) if reserve3 is not None else 0 + + if (reserve3_int & (1 << 8)) != 0: + ret_wdr, is_open_wdr_data = self.session.getWdrMode() + mode = is_open_wdr_data["mode"] if ret_wdr == 0 and is_open_wdr_data else 0 + is_open_wdr = bool(int(mode)) + else: + ret_hdr, is_open_hdr_data = self.session.getHdrMode() + mode = is_open_hdr_data["mode"] if ret_hdr == 0 and is_open_hdr_data else 0 + is_open_hdr = bool(int(mode)) + + return FoscamDeviceInfo( + dev_info=dev_info, + product_info=product_info, + is_open_ir=is_open_ir, + is_flip=is_flip, + is_mirror=is_mirror, + is_asleep=is_asleep, + is_open_white_light=is_open_white_light_val, + is_siren_alarm=is_siren_alarm_val, + volume=volume_val, + speak_volume=speak_volume_val, + is_turn_off_volume=is_turn_off_volume_val, + is_turn_off_light=is_turn_off_light_val, + is_open_wdr=is_open_wdr, + is_open_hdr=is_open_hdr, + ) + + async def _async_update_data(self) -> FoscamDeviceInfo: """Fetch data from API endpoint.""" - - async with asyncio.timeout(30): - data = {} - ret, dev_info = await self.hass.async_add_executor_job( - self.session.get_dev_info - ) - if ret == 0: - data["dev_info"] = dev_info - - all_info = await self.hass.async_add_executor_job( - self.session.get_product_all_info - ) - data["product_info"] = all_info[1] - - ret, is_asleep = await self.hass.async_add_executor_job( - self.session.is_asleep - ) - data["is_asleep"] = {"supported": ret == 0, "status": is_asleep} - return data + async with asyncio.timeout(10): + return await self.hass.async_add_executor_job(self.gather_all_configs) diff --git a/homeassistant/components/foscam/entity.py b/homeassistant/components/foscam/entity.py index e9d1bbbe176..7bc983cbfaa 100644 --- a/homeassistant/components/foscam/entity.py +++ b/homeassistant/components/foscam/entity.py @@ -13,19 +13,15 @@ from .coordinator import FoscamCoordinator class FoscamEntity(CoordinatorEntity[FoscamCoordinator]): """Base entity for Foscam camera.""" - def __init__( - self, - coordinator: FoscamCoordinator, - entry_id: str, - ) -> None: + def __init__(self, coordinator: FoscamCoordinator, config_entry_id: str) -> None: """Initialize the base Foscam entity.""" super().__init__(coordinator) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, entry_id)}, + identifiers={(DOMAIN, config_entry_id)}, manufacturer="Foscam", ) - if dev_info := coordinator.data.get("dev_info"): + if dev_info := coordinator.data.dev_info: self._attr_device_info[ATTR_MODEL] = dev_info["productName"] self._attr_device_info[ATTR_SW_VERSION] = dev_info["firmwareVer"] self._attr_device_info[ATTR_HW_VERSION] = dev_info["hardwareVer"] diff --git a/homeassistant/components/foscam/icons.json b/homeassistant/components/foscam/icons.json index 437575024d1..4b0b0c17c32 100644 --- a/homeassistant/components/foscam/icons.json +++ b/homeassistant/components/foscam/icons.json @@ -6,5 +6,39 @@ "ptz_preset": { "service": "mdi:target-variant" } + }, + "entity": { + "switch": { + "flip_switch": { + "default": "mdi:flip-vertical" + }, + "mirror_switch": { + "default": "mdi:mirror" + }, + "ir_switch": { + "default": "mdi:theme-light-dark" + }, + "sleep_switch": { + "default": "mdi:sleep" + }, + "white_light_switch": { + "default": "mdi:light-flood-down" + }, + "siren_alarm_switch": { + "default": "mdi:alarm-note" + }, + "turn_off_volume_switch": { + "default": "mdi:volume-off" + }, + "turn_off_light_switch": { + "default": "mdi:lightbulb-fluorescent-tube" + }, + "hdr_switch": { + "default": "mdi:hdr" + }, + "wdr_switch": { + "default": "mdi:alpha-w-box" + } + } } } diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index 9e6864cf1c6..87112199b0f 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/foscam", "iot_class": "local_polling", "loggers": ["libpyfoscamcgi"], - "requirements": ["libpyfoscamcgi==0.0.6"] + "requirements": ["libpyfoscamcgi==0.0.7"] } diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json index 03351e3238f..d73833b1cae 100644 --- a/homeassistant/components/foscam/strings.json +++ b/homeassistant/components/foscam/strings.json @@ -11,7 +11,12 @@ "stream": "Stream" }, "data_description": { - "host": "The hostname or IP address of your Foscam camera." + "host": "The hostname or IP address of your Foscam camera.", + "port": "The port of your Foscam camera, default is 88.", + "username": "The username to log in to your Foscam camera.", + "password": "The password to log in to your Foscam camera.", + "rtsp_port": "The RTSP protocol port of the camera, used to pull the camera's real-time video stream. New model cameras only support RTSP ports 88 and 554, while old model cameras only support ports 88 and 65534.", + "stream": "Select the video stream type to pull. The main stream offers higher clarity but requires a better network environment." } } }, @@ -27,8 +32,35 @@ }, "entity": { "switch": { + "flip_switch": { + "name": "Flip" + }, + "mirror_switch": { + "name": "Mirror" + }, + "ir_switch": { + "name": "Infrared mode" + }, "sleep_switch": { - "name": "Sleep" + "name": "Sleep mode" + }, + "white_light_switch": { + "name": "White light" + }, + "siren_alarm_switch": { + "name": "Siren alarm" + }, + "turn_off_volume_switch": { + "name": "Volume muted" + }, + "turn_off_light_switch": { + "name": "Light" + }, + "hdr_switch": { + "name": "HDR" + }, + "wdr_switch": { + "name": "WDR" } } }, diff --git a/homeassistant/components/foscam/switch.py b/homeassistant/components/foscam/switch.py index 24b05b5aeaa..91118a27277 100644 --- a/homeassistant/components/foscam/switch.py +++ b/homeassistant/components/foscam/switch.py @@ -2,18 +2,117 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError +from libpyfoscamcgi import FoscamCamera + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import LOGGER from .coordinator import FoscamConfigEntry, FoscamCoordinator from .entity import FoscamEntity +def handle_ir_turn_on(session: FoscamCamera) -> None: + """Turn on IR LED: sets IR mode to auto (if supported), then turns off the IR LED.""" + + session.set_infra_led_config(1) + session.open_infra_led() + + +def handle_ir_turn_off(session: FoscamCamera) -> None: + """Turn off IR LED: sets IR mode to manual (if supported), then turns open the IR LED.""" + + session.set_infra_led_config(0) + session.close_infra_led() + + +@dataclass(frozen=True, kw_only=True) +class FoscamSwitchEntityDescription(SwitchEntityDescription): + """A custom entity description that supports a turn_off function.""" + + native_value_fn: Callable[..., bool] + turn_off_fn: Callable[[FoscamCamera], None] + turn_on_fn: Callable[[FoscamCamera], None] + + +SWITCH_DESCRIPTIONS: list[FoscamSwitchEntityDescription] = [ + FoscamSwitchEntityDescription( + key="is_flip", + translation_key="flip_switch", + native_value_fn=lambda data: data.is_flip, + turn_off_fn=lambda session: session.flip_video(0), + turn_on_fn=lambda session: session.flip_video(1), + ), + FoscamSwitchEntityDescription( + key="is_mirror", + translation_key="mirror_switch", + native_value_fn=lambda data: data.is_mirror, + turn_off_fn=lambda session: session.mirror_video(0), + turn_on_fn=lambda session: session.mirror_video(1), + ), + FoscamSwitchEntityDescription( + key="is_open_ir", + translation_key="ir_switch", + native_value_fn=lambda data: data.is_open_ir, + turn_off_fn=handle_ir_turn_off, + turn_on_fn=handle_ir_turn_on, + ), + FoscamSwitchEntityDescription( + key="sleep_switch", + translation_key="sleep_switch", + native_value_fn=lambda data: data.is_asleep["status"], + turn_off_fn=lambda session: session.wake_up(), + turn_on_fn=lambda session: session.sleep(), + ), + FoscamSwitchEntityDescription( + key="is_open_white_light", + translation_key="white_light_switch", + native_value_fn=lambda data: data.is_open_white_light, + turn_off_fn=lambda session: session.closeWhiteLight(), + turn_on_fn=lambda session: session.openWhiteLight(), + ), + FoscamSwitchEntityDescription( + key="is_siren_alarm", + translation_key="siren_alarm_switch", + native_value_fn=lambda data: data.is_siren_alarm, + turn_off_fn=lambda session: session.setSirenConfig(0, 100, 0), + turn_on_fn=lambda session: session.setSirenConfig(1, 100, 0), + ), + FoscamSwitchEntityDescription( + key="is_turn_off_volume", + translation_key="turn_off_volume_switch", + native_value_fn=lambda data: data.is_turn_off_volume, + turn_off_fn=lambda session: session.setVoiceEnableState(1), + turn_on_fn=lambda session: session.setVoiceEnableState(0), + ), + FoscamSwitchEntityDescription( + key="is_turn_off_light", + translation_key="turn_off_light_switch", + native_value_fn=lambda data: data.is_turn_off_light, + turn_off_fn=lambda session: session.setLedEnableState(0), + turn_on_fn=lambda session: session.setLedEnableState(1), + ), + FoscamSwitchEntityDescription( + key="is_open_hdr", + translation_key="hdr_switch", + native_value_fn=lambda data: data.is_open_hdr, + turn_off_fn=lambda session: session.setHdrMode(0), + turn_on_fn=lambda session: session.setHdrMode(1), + ), + FoscamSwitchEntityDescription( + key="is_open_wdr", + translation_key="wdr_switch", + native_value_fn=lambda data: data.is_open_wdr, + turn_off_fn=lambda session: session.setWdrMode(0), + turn_on_fn=lambda session: session.setWdrMode(1), + ), +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: FoscamConfigEntry, @@ -22,63 +121,61 @@ async def async_setup_entry( """Set up foscam switch from a config entry.""" coordinator = config_entry.runtime_data - await coordinator.async_config_entry_first_refresh() - if coordinator.data["is_asleep"]["supported"]: - async_add_entities([FoscamSleepSwitch(coordinator, config_entry)]) + entities = [] + + product_info = coordinator.data.product_info + reserve3 = product_info.get("reserve3", "0") + + for description in SWITCH_DESCRIPTIONS: + if description.key == "is_asleep": + if not coordinator.data.is_asleep["supported"]: + continue + elif description.key == "is_open_hdr": + if ((1 << 8) & int(reserve3)) != 0 or ((1 << 7) & int(reserve3)) == 0: + continue + elif description.key == "is_open_wdr": + if ((1 << 8) & int(reserve3)) == 0: + continue + + entities.append(FoscamGenericSwitch(coordinator, description)) + async_add_entities(entities) -class FoscamSleepSwitch(FoscamEntity, SwitchEntity): - """An implementation for Sleep Switch.""" +class FoscamGenericSwitch(FoscamEntity, SwitchEntity): + """A generic switch class for Foscam entities.""" + + _attr_has_entity_name = True + entity_description: FoscamSwitchEntityDescription def __init__( self, coordinator: FoscamCoordinator, - config_entry: FoscamConfigEntry, + description: FoscamSwitchEntityDescription, ) -> None: - """Initialize a Foscam Sleep Switch.""" - super().__init__(coordinator, config_entry.entry_id) + """Initialize the generic switch.""" + entry_id = coordinator.config_entry.entry_id + super().__init__(coordinator, entry_id) - self._attr_unique_id = f"{config_entry.entry_id}_sleep_switch" - self._attr_translation_key = "sleep_switch" - self._attr_has_entity_name = True - - self.is_asleep = self.coordinator.data["is_asleep"]["status"] + self.entity_description = description + self._attr_unique_id = f"{entry_id}_{description.key}" @property - def is_on(self): - """Return true if camera is asleep.""" - return self.is_asleep + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.entity_description.native_value_fn(self.coordinator.data) async def async_turn_off(self, **kwargs: Any) -> None: - """Wake camera.""" - LOGGER.debug("Wake camera") - - ret, _ = await self.hass.async_add_executor_job( - self.coordinator.session.wake_up + """Turn off the entity.""" + self.hass.async_add_executor_job( + self.entity_description.turn_off_fn, self.coordinator.session ) - - if ret != 0: - raise HomeAssistantError(f"Error waking up: {ret}") - await self.coordinator.async_request_refresh() async def async_turn_on(self, **kwargs: Any) -> None: - """But camera is sleep.""" - LOGGER.debug("Sleep camera") - - ret, _ = await self.hass.async_add_executor_job(self.coordinator.session.sleep) - - if ret != 0: - raise HomeAssistantError(f"Error sleeping: {ret}") - + """Turn on the entity.""" + self.hass.async_add_executor_job( + self.entity_description.turn_on_fn, self.coordinator.session + ) await self.coordinator.async_request_refresh() - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - - self.is_asleep = self.coordinator.data["is_asleep"]["status"] - - self.async_write_ha_state() diff --git a/homeassistant/components/freebox/alarm_control_panel.py b/homeassistant/components/freebox/alarm_control_panel.py index b0242a1b054..968f3dc16a6 100644 --- a/homeassistant/components/freebox/alarm_control_panel.py +++ b/homeassistant/components/freebox/alarm_control_panel.py @@ -36,7 +36,7 @@ async def async_setup_entry( async_add_entities( ( - FreeboxAlarm(hass, router, node) + FreeboxAlarm(router, node) for node in router.home_devices.values() if node["category"] == FreeboxHomeCategory.ALARM ), @@ -49,11 +49,9 @@ class FreeboxAlarm(FreeboxHomeEntity, AlarmControlPanelEntity): _attr_code_arm_required = False - def __init__( - self, hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any] - ) -> None: + def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None: """Initialize an alarm.""" - super().__init__(hass, router, node) + super().__init__(router, node) # Commands self._command_trigger = self.get_command_id( diff --git a/homeassistant/components/freebox/binary_sensor.py b/homeassistant/components/freebox/binary_sensor.py index 75b7dded36a..3b262309361 100644 --- a/homeassistant/components/freebox/binary_sensor.py +++ b/homeassistant/components/freebox/binary_sensor.py @@ -50,12 +50,12 @@ async def async_setup_entry( for node in router.home_devices.values(): if node["category"] == FreeboxHomeCategory.PIR: - binary_entities.append(FreeboxPirSensor(hass, router, node)) + binary_entities.append(FreeboxPirSensor(router, node)) elif node["category"] == FreeboxHomeCategory.DWS: - binary_entities.append(FreeboxDwsSensor(hass, router, node)) + binary_entities.append(FreeboxDwsSensor(router, node)) binary_entities.extend( - FreeboxCoverSensor(hass, router, node) + FreeboxCoverSensor(router, node) for endpoint in node["show_endpoints"] if ( endpoint["name"] == "cover" @@ -74,13 +74,12 @@ class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity): def __init__( self, - hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any], sub_node: dict[str, Any] | None = None, ) -> None: """Initialize a Freebox binary sensor.""" - super().__init__(hass, router, node, sub_node) + super().__init__(router, node, sub_node) self._command_id = self.get_command_id( node["type"]["endpoints"], "signal", self._sensor_name ) @@ -123,9 +122,7 @@ class FreeboxCoverSensor(FreeboxHomeBinarySensor): _sensor_name = "cover" - def __init__( - self, hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any] - ) -> None: + def __init__(self, router: FreeboxRouter, node: dict[str, Any]) -> None: """Initialize a cover for another device.""" cover_node = next( filter( @@ -134,7 +131,7 @@ class FreeboxCoverSensor(FreeboxHomeBinarySensor): ), None, ) - super().__init__(hass, router, node, cover_node) + super().__init__(router, node, cover_node) class FreeboxRaidDegradedSensor(BinarySensorEntity): diff --git a/homeassistant/components/freebox/camera.py b/homeassistant/components/freebox/camera.py index d997908dd06..f7e078f0736 100644 --- a/homeassistant/components/freebox/camera.py +++ b/homeassistant/components/freebox/camera.py @@ -74,7 +74,7 @@ class FreeboxCamera(FreeboxHomeEntity, FFmpegCamera): ) -> None: """Initialize a camera.""" - super().__init__(hass, router, node) + super().__init__(router, node) device_info = { CONF_NAME: node["label"].strip(), CONF_INPUT: node["props"]["Stream"], diff --git a/homeassistant/components/freebox/entity.py b/homeassistant/components/freebox/entity.py index 129186fd50b..17cd30f40ea 100644 --- a/homeassistant/components/freebox/entity.py +++ b/homeassistant/components/freebox/entity.py @@ -2,11 +2,9 @@ from __future__ import annotations -from collections.abc import Callable import logging from typing import Any -from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -22,13 +20,11 @@ class FreeboxHomeEntity(Entity): def __init__( self, - hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any], sub_node: dict[str, Any] | None = None, ) -> None: """Initialize a Freebox Home entity.""" - self._hass = hass self._router = router self._node = node self._sub_node = sub_node @@ -44,7 +40,6 @@ class FreeboxHomeEntity(Entity): self._available = True self._firmware = node["props"].get("FwVersion") self._manufacturer = "Freebox SAS" - self._remove_signal_update: Callable[[], None] | None = None self._model = CATEGORY_TO_MODEL.get(node["category"]) if self._model is None: @@ -61,10 +56,7 @@ class FreeboxHomeEntity(Entity): model=self._model, name=self._device_name, sw_version=self._firmware, - via_device=( - DOMAIN, - router.mac, - ), + via_device=(DOMAIN, router.mac), ) async def async_update_signal(self) -> None: @@ -116,23 +108,14 @@ class FreeboxHomeEntity(Entity): async def async_added_to_hass(self) -> None: """Register state update callback.""" - self.remove_signal_update( + self.async_on_remove( async_dispatcher_connect( - self._hass, + self.hass, self._router.signal_home_device_update, self.async_update_signal, ) ) - async def async_will_remove_from_hass(self) -> None: - """When entity will be removed from hass.""" - if self._remove_signal_update is not None: - self._remove_signal_update() - - def remove_signal_update(self, dispatcher: Callable[[], None]) -> None: - """Register state update callback.""" - self._remove_signal_update = dispatcher - def get_value(self, ep_type: str, name: str): """Get the value.""" node = next( diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index d6c45cd178b..b2eb329b545 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -115,8 +115,10 @@ class FreeboxRouter: self._api: Freepybox = api self.name: str = freebox_config["model_info"]["pretty_name"] + self.model_id: str = freebox_config["model_info"]["name"] self.mac: str = freebox_config["mac"] self._sw_v: str = freebox_config["firmware_version"] + self._hw_v: str | None = freebox_config.get("board_name") self._attrs: dict[str, Any] = {} self.supports_hosts = True @@ -282,7 +284,10 @@ class FreeboxRouter: identifiers={(DOMAIN, self.mac)}, manufacturer="Freebox SAS", name=self.name, + model=self.name, + model_id=self.model_id, sw_version=self._sw_v, + hw_version=self._hw_v, ) @property diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 45fe18db95a..53314549f57 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -68,7 +68,6 @@ async def async_setup_entry( ) -> None: """Set up the sensors.""" router = entry.runtime_data - entities: list[SensorEntity] = [] _LOGGER.debug( "%s - %s - %s temperature sensors", @@ -76,7 +75,7 @@ async def async_setup_entry( router.mac, len(router.sensors_temperature), ) - entities = [ + entities: list[SensorEntity] = [ FreeboxSensor( router, SensorEntityDescription( @@ -105,14 +104,16 @@ async def async_setup_entry( for description in DISK_PARTITION_SENSORS ) - for node in router.home_devices.values(): - for endpoint in node["show_endpoints"]: - if ( - endpoint["name"] == "battery" - and endpoint["ep_type"] == "signal" - and endpoint.get("value") is not None - ): - entities.append(FreeboxBatterySensor(hass, router, node, endpoint)) + entities.extend( + FreeboxBatterySensor(router, node, endpoint) + for node in router.home_devices.values() + for endpoint in node["show_endpoints"] + if ( + endpoint["name"] == "battery" + and endpoint["ep_type"] == "signal" + and endpoint.get("value") is not None + ) + ) if entities: async_add_entities(entities, True) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index d8d3bbd7a53..25687f0061a 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -120,7 +120,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): self.fritz_guest_wifi: FritzGuestWLAN = None self.fritz_hosts: FritzHosts = None self.fritz_status: FritzStatus = None - self.hass = hass self.host = host self.mesh_role = MeshRoles.NONE self.mesh_wifi_uplink = False diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2f2a8e93b1e..ff50567257a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -50,7 +50,7 @@ CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5" CONF_FRONTEND_REPO = "development_repo" CONF_JS_VERSION = "javascript_version" -DEFAULT_THEME_COLOR = "#03A9F4" +DEFAULT_THEME_COLOR = "#2980b9" DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels") diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 61ca88ba70a..9fc80cf0e8a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250806.0"] + "requirements": ["home-assistant-frontend==20250811.1"] } diff --git a/homeassistant/components/fyta/const.py b/homeassistant/components/fyta/const.py index bf4636a713a..9e1898f5ae6 100644 --- a/homeassistant/components/fyta/const.py +++ b/homeassistant/components/fyta/const.py @@ -2,3 +2,8 @@ DOMAIN = "fyta" CONF_EXPIRATION = "expiration" + +CONF_MAX_ACCEPTABLE = "max_acceptable" +CONF_MAX_GOOD = "max_good" +CONF_MIN_ACCEPTABLE = "min_acceptable" +CONF_MIN_GOOD = "min_good" diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 622945ae102..d16a3eccfff 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -25,6 +25,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType +from .const import ( + CONF_MAX_ACCEPTABLE, + CONF_MAX_GOOD, + CONF_MIN_ACCEPTABLE, + CONF_MIN_GOOD, +) from .coordinator import FytaConfigEntry, FytaCoordinator from .entity import FytaPlantEntity @@ -36,6 +42,13 @@ class FytaSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[Plant], StateType | datetime] +@dataclass(frozen=True, kw_only=True) +class FytaMeasurementSensorEntityDescription(FytaSensorEntityDescription): + """Describes Fyta sensor entity.""" + + attribute_fn: Callable[[Plant], dict[str, float | None]] + + PLANT_STATUS_LIST: list[str] = ["deleted", "doing_great", "need_attention", "no_sensor"] PLANT_MEASUREMENT_STATUS_LIST: list[str] = [ "no_data", @@ -95,35 +108,6 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ options=PLANT_MEASUREMENT_STATUS_LIST, value_fn=lambda plant: plant.salinity_status.name.lower(), ), - FytaSensorEntityDescription( - key="temperature", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda plant: plant.temperature, - ), - FytaSensorEntityDescription( - key="light", - translation_key="light", - native_unit_of_measurement="μmol/s⋅m²", - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda plant: plant.light, - ), - FytaSensorEntityDescription( - key="moisture", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.MOISTURE, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda plant: plant.moisture, - ), - FytaSensorEntityDescription( - key="salinity", - translation_key="salinity", - native_unit_of_measurement=UnitOfConductivity.MILLISIEMENS_PER_CM, - device_class=SensorDeviceClass.CONDUCTIVITY, - state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda plant: plant.salinity, - ), FytaSensorEntityDescription( key="ph", device_class=SensorDeviceClass.PH, @@ -152,6 +136,62 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ ), ] +MEASUREMENT_SENSORS: Final[list[FytaMeasurementSensorEntityDescription]] = [ + FytaMeasurementSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + attribute_fn=lambda plant: { + CONF_MAX_ACCEPTABLE: plant.temperature_max_acceptable, + CONF_MAX_GOOD: plant.temperature_max_good, + CONF_MIN_ACCEPTABLE: plant.temperature_min_acceptable, + CONF_MIN_GOOD: plant.temperature_min_good, + }, + value_fn=lambda plant: plant.temperature, + ), + FytaMeasurementSensorEntityDescription( + key="light", + translation_key="light", + native_unit_of_measurement="μmol/s⋅m²", + state_class=SensorStateClass.MEASUREMENT, + attribute_fn=lambda plant: { + CONF_MAX_ACCEPTABLE: plant.light_max_acceptable, + CONF_MAX_GOOD: plant.light_max_good, + CONF_MIN_ACCEPTABLE: plant.light_min_acceptable, + CONF_MIN_GOOD: plant.light_min_good, + }, + value_fn=lambda plant: plant.light, + ), + FytaMeasurementSensorEntityDescription( + key="moisture", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.MOISTURE, + state_class=SensorStateClass.MEASUREMENT, + attribute_fn=lambda plant: { + CONF_MAX_ACCEPTABLE: plant.moisture_max_acceptable, + CONF_MAX_GOOD: plant.moisture_max_good, + CONF_MIN_ACCEPTABLE: plant.moisture_min_acceptable, + CONF_MIN_GOOD: plant.moisture_min_good, + }, + value_fn=lambda plant: plant.moisture, + ), + FytaMeasurementSensorEntityDescription( + key="salinity", + translation_key="salinity", + native_unit_of_measurement=UnitOfConductivity.MILLISIEMENS_PER_CM, + device_class=SensorDeviceClass.CONDUCTIVITY, + state_class=SensorStateClass.MEASUREMENT, + attribute_fn=lambda plant: { + CONF_MAX_ACCEPTABLE: plant.salinity_max_acceptable, + CONF_MAX_GOOD: plant.salinity_max_good, + CONF_MIN_ACCEPTABLE: plant.salinity_min_acceptable, + CONF_MIN_GOOD: plant.salinity_min_good, + }, + value_fn=lambda plant: plant.salinity, + ), +] + async def async_setup_entry( hass: HomeAssistant, @@ -168,14 +208,28 @@ async def async_setup_entry( if sensor.key in dir(coordinator.data.get(plant_id)) ] + plant_entities.extend( + FytaPlantMeasurementSensor(coordinator, entry, sensor, plant_id) + for plant_id in coordinator.fyta.plant_list + for sensor in MEASUREMENT_SENSORS + if sensor.key in dir(coordinator.data.get(plant_id)) + ) + async_add_entities(plant_entities) def _async_add_new_device(plant_id: int) -> None: - async_add_entities( + plant_entities = [ FytaPlantSensor(coordinator, entry, sensor, plant_id) for sensor in SENSORS if sensor.key in dir(coordinator.data.get(plant_id)) + ] + + plant_entities.extend( + FytaPlantMeasurementSensor(coordinator, entry, sensor, plant_id) + for sensor in MEASUREMENT_SENSORS + if sensor.key in dir(coordinator.data.get(plant_id)) ) + async_add_entities(plant_entities) coordinator.new_device_callbacks.append(_async_add_new_device) @@ -190,3 +244,15 @@ class FytaPlantSensor(FytaPlantEntity, SensorEntity): """Return the state for this sensor.""" return self.entity_description.value_fn(self.plant) + + +class FytaPlantMeasurementSensor(FytaPlantSensor): + """Represents a Fyta measurement sensor.""" + + entity_description: FytaMeasurementSensorEntityDescription + + @property + def extra_state_attributes(self) -> dict[str, float | None]: + """Return the device state attributes.""" + + return self.entity_description.attribute_fn(self.plant) diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json index 67bb991a437..b0c14e0d4c1 100644 --- a/homeassistant/components/fyta/strings.json +++ b/homeassistant/components/fyta/strings.json @@ -138,10 +138,64 @@ } }, "light": { - "name": "Light" + "name": "Light", + "state_attributes": { + "max_acceptable": { "name": "Maximum acceptable" }, + "max_good": { "name": "Maximum good" }, + "min_acceptable": { "name": "Minimum acceptable" }, + "min_good": { "name": "Minimum good" } + } + }, + "moisture": { + "name": "[%key:component::sensor::entity_component::moisture::name%]", + "state_attributes": { + "max_acceptable": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::max_acceptable::name%]" + }, + "max_good": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::max_good::name%]" + }, + "min_acceptable": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::min_acceptable::name%]" + }, + "min_good": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::min_good::name%]" + } + } }, "salinity": { - "name": "Salinity" + "name": "Salinity", + "state_attributes": { + "max_acceptable": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::max_acceptable::name%]" + }, + "max_good": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::max_good::name%]" + }, + "min_acceptable": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::min_acceptable::name%]" + }, + "min_good": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::min_good::name%]" + } + } + }, + "temperature": { + "name": "[%key:component::sensor::entity_component::temperature::name%]", + "state_attributes": { + "max_acceptable": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::max_acceptable::name%]" + }, + "max_good": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::max_good::name%]" + }, + "min_acceptable": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::min_acceptable::name%]" + }, + "min_good": { + "name": "[%key:component::fyta::entity::sensor::light::state_attributes::min_good::name%]" + } + } }, "last_fertilised": { "name": "Last fertilized" diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 28cf40aae6e..5df8fe1b2e4 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -29,7 +29,6 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self, hass: HomeAssistant, entry: GlancesConfigEntry, api: Glances ) -> None: """Initialize the Glances data.""" - self.hass = hass self.host: str = entry.data[CONF_HOST] self.api = api super().__init__( diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 3c1c9cad0b0..a1fd5ea0f9b 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -124,7 +124,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: f"Error generating content due to content violations, reason: {response.prompt_feedback.block_reason_message}" ) - if not response.candidates[0].content.parts: + if ( + not response.candidates + or not response.candidates[0].content + or not response.candidates[0].content.parts + ): raise HomeAssistantError("Unknown error generating content") return {"text": response.text} diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index e760187bc66..9048304a006 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -377,7 +377,7 @@ async def google_generative_ai_config_option_schema( value=api_model.name, ) for api_model in sorted( - api_models, key=lambda x: x.name.lstrip("models/") or "" + api_models, key=lambda x: (x.name or "").lstrip("models/") ) if ( api_model.name diff --git a/homeassistant/components/google_generative_ai_conversation/entity.py b/homeassistant/components/google_generative_ai_conversation/entity.py index 8e967d84517..90c144530e0 100644 --- a/homeassistant/components/google_generative_ai_conversation/entity.py +++ b/homeassistant/components/google_generative_ai_conversation/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import codecs -from collections.abc import AsyncGenerator, Callable +from collections.abc import AsyncGenerator, AsyncIterator, Callable from dataclasses import replace import mimetypes from pathlib import Path @@ -15,6 +15,7 @@ from google.genai.errors import APIError, ClientError from google.genai.types import ( AutomaticFunctionCallingConfig, Content, + ContentDict, File, FileState, FunctionDeclaration, @@ -23,9 +24,11 @@ from google.genai.types import ( GoogleSearch, HarmCategory, Part, + PartUnionDict, SafetySetting, Schema, Tool, + ToolListUnion, ) import voluptuous as vol from voluptuous_openapi import convert @@ -237,7 +240,7 @@ def _convert_content( async def _transform_stream( - result: AsyncGenerator[GenerateContentResponse], + result: AsyncIterator[GenerateContentResponse], ) -> AsyncGenerator[conversation.AssistantContentDeltaDict]: new_message = True try: @@ -342,7 +345,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity): """Generate an answer for the chat log.""" options = self.subentry.data - tools: list[Tool | Callable[..., Any]] | None = None + tools: ToolListUnion | None = None if chat_log.llm_api: tools = [ _format_tool(tool, chat_log.llm_api.custom_serializer) @@ -373,7 +376,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity): else: raise HomeAssistantError("Invalid prompt content") - messages: list[Content] = [] + messages: list[Content | ContentDict] = [] # Google groups tool results, we do not. Group them before sending. tool_results: list[conversation.ToolResultContent] = [] @@ -400,7 +403,10 @@ class GoogleGenerativeAILLMBaseEntity(Entity): # The SDK requires the first message to be a user message # This is not the case if user used `start_conversation` # Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537 - if messages and messages[0].role != "user": + if messages and ( + (isinstance(messages[0], Content) and messages[0].role != "user") + or (isinstance(messages[0], dict) and messages[0]["role"] != "user") + ): messages.insert( 0, Content(role="user", parts=[Part.from_text(text=" ")]), @@ -440,14 +446,14 @@ class GoogleGenerativeAILLMBaseEntity(Entity): ) user_message = chat_log.content[-1] assert isinstance(user_message, conversation.UserContent) - chat_request: str | list[Part] = user_message.content + chat_request: list[PartUnionDict] = [user_message.content] if user_message.attachments: files = await async_prepare_files_for_prompt( self.hass, self._genai_client, [a.path for a in user_message.attachments], ) - chat_request = [chat_request, *files] + chat_request = [*chat_request, *files] # To prevent infinite loops, we limit the number of iterations for _iteration in range(MAX_TOOL_ITERATIONS): @@ -464,15 +470,17 @@ class GoogleGenerativeAILLMBaseEntity(Entity): error = ERROR_GETTING_RESPONSE raise HomeAssistantError(error) from err - chat_request = _create_google_tool_response_parts( - [ - content - async for content in chat_log.async_add_delta_content_stream( - self.entity_id, - _transform_stream(chat_response_generator), - ) - if isinstance(content, conversation.ToolResultContent) - ] + chat_request = list( + _create_google_tool_response_parts( + [ + content + async for content in chat_log.async_add_delta_content_stream( + self.entity_id, + _transform_stream(chat_response_generator), + ) + if isinstance(content, conversation.ToolResultContent) + ] + ) ) if not chat_log.unresponded_tool_results: @@ -559,13 +567,13 @@ async def async_prepare_files_for_prompt( await asyncio.sleep(FILE_POLLING_INTERVAL_SECONDS) uploaded_file = await client.aio.files.get( - name=uploaded_file.name, + name=uploaded_file.name or "", config={"http_options": {"timeout": TIMEOUT_MILLIS}}, ) if uploaded_file.state == FileState.FAILED: raise HomeAssistantError( - f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message}" + f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message if uploaded_file.error else 'unknown'}" ) prompt_parts = await hass.async_add_executor_job(upload_files) diff --git a/homeassistant/components/google_generative_ai_conversation/manifest.json b/homeassistant/components/google_generative_ai_conversation/manifest.json index 25e44964a6d..ce089440b97 100644 --- a/homeassistant/components/google_generative_ai_conversation/manifest.json +++ b/homeassistant/components/google_generative_ai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["google-genai==1.7.0"] + "requirements": ["google-genai==1.29.0"] } diff --git a/homeassistant/components/google_generative_ai_conversation/tts.py b/homeassistant/components/google_generative_ai_conversation/tts.py index 08e83242fcd..ed956bdb13c 100644 --- a/homeassistant/components/google_generative_ai_conversation/tts.py +++ b/homeassistant/components/google_generative_ai_conversation/tts.py @@ -146,15 +146,41 @@ class GoogleGenerativeAITextToSpeechEntity( ) ) ) + + def _extract_audio_parts( + response: types.GenerateContentResponse, + ) -> tuple[bytes, str]: + if ( + not response.candidates + or not response.candidates[0].content + or not response.candidates[0].content.parts + or not response.candidates[0].content.parts[0].inline_data + ): + raise ValueError("No content returned from TTS generation") + + data = response.candidates[0].content.parts[0].inline_data.data + mime_type = response.candidates[0].content.parts[0].inline_data.mime_type + + if not isinstance(data, bytes): + raise TypeError( + f"Expected bytes for audio data, got {type(data).__name__}" + ) + if not isinstance(mime_type, str): + raise TypeError( + f"Expected str for mime_type, got {type(mime_type).__name__}" + ) + + return data, mime_type + try: response = await self._genai_client.aio.models.generate_content( model=self.subentry.data.get(CONF_CHAT_MODEL, RECOMMENDED_TTS_MODEL), contents=message, config=config, ) - data = response.candidates[0].content.parts[0].inline_data.data - mime_type = response.candidates[0].content.parts[0].inline_data.mime_type - except (APIError, ClientError, ValueError) as exc: + + data, mime_type = _extract_audio_parts(response) + except (APIError, ClientError, ValueError, TypeError) as exc: LOGGER.error("Error during TTS: %s", exc, exc_info=True) raise HomeAssistantError(exc) from exc return "wav", convert_to_wav(data, mime_type) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 152e629be2e..88f7d9017ab 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -146,6 +146,20 @@ async def light_switch_options_schema( ) +LIGHT_CONFIG_SCHEMA = basic_group_config_schema("light").extend( + { + vol.Required(CONF_ALL, default=False): selector.BooleanSelector(), + } +) + + +SWITCH_CONFIG_SCHEMA = basic_group_config_schema("switch").extend( + { + vol.Required(CONF_ALL, default=False): selector.BooleanSelector(), + } +) + + GROUP_TYPES = [ "binary_sensor", "button", @@ -210,7 +224,7 @@ CONFIG_FLOW = { validate_user_input=set_group_type("fan"), ), "light": SchemaFlowFormStep( - basic_group_config_schema("light"), + LIGHT_CONFIG_SCHEMA, preview="group", validate_user_input=set_group_type("light"), ), @@ -235,7 +249,7 @@ CONFIG_FLOW = { validate_user_input=set_group_type("sensor"), ), "switch": SchemaFlowFormStep( - basic_group_config_schema("switch"), + SWITCH_CONFIG_SCHEMA, preview="group", validate_user_input=set_group_type("switch"), ), diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index bb9ab4b25d8..8a9f4377a62 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -66,9 +66,13 @@ "light": { "title": "[%key:component::group::config::step::user::title%]", "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:common::config_flow::data::name%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } }, "lock": { @@ -115,9 +119,13 @@ "switch": { "title": "[%key:component::group::config::step::user::title%]", "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:common::config_flow::data::name%]" + }, + "data_description": { + "all": "[%key:component::group::config::step::binary_sensor::data_description::all%]" } } } diff --git a/homeassistant/components/guardian/entity.py b/homeassistant/components/guardian/entity.py index c48c87afa01..760b9423afd 100644 --- a/homeassistant/components/guardian/entity.py +++ b/homeassistant/components/guardian/entity.py @@ -74,7 +74,7 @@ class ValveControllerEntity(GuardianEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.data[CONF_UID])}, manufacturer="Elexa", - model=self._diagnostics_coordinator.data["firmware"], + sw_version=self._diagnostics_coordinator.data["firmware"], name=f"Guardian valve controller {entry.data[CONF_UID]}", ) self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 217b5e739d1..514a12d26b7 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,19 +1,26 @@ """The habitica integration.""" +from uuid import UUID + from habiticalib import Habitica from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType +from homeassistant.util.hass_dict import HassKey from .const import CONF_API_USER, DOMAIN, X_CLIENT -from .coordinator import HabiticaConfigEntry, HabiticaDataUpdateCoordinator +from .coordinator import ( + HabiticaConfigEntry, + HabiticaDataUpdateCoordinator, + HabiticaPartyCoordinator, +) from .services import async_setup_services CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) - +HABITICA_KEY: HassKey[dict[UUID, HabiticaPartyCoordinator]] = HassKey(DOMAIN) PLATFORMS = [ Platform.BINARY_SENSOR, @@ -37,6 +44,8 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: HabiticaConfigEntry ) -> bool: """Set up habitica from a config entry.""" + party_added_by_this_entry: UUID | None = None + device_reg = dr.async_get(hass) session = async_get_clientsession( hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True) @@ -54,11 +63,53 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() config_entry.runtime_data = coordinator - await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + party = coordinator.data.user.party.id + if HABITICA_KEY not in hass.data: + hass.data[HABITICA_KEY] = {} + + if party is not None and party not in hass.data[HABITICA_KEY]: + party_coordinator = HabiticaPartyCoordinator(hass, config_entry, api) + await party_coordinator.async_config_entry_first_refresh() + + hass.data[HABITICA_KEY][party] = party_coordinator + party_added_by_this_entry = party + + @callback + def _party_update_listener() -> None: + """On party change, unload coordinator, remove device and reload.""" + nonlocal party, party_added_by_this_entry + party_updated = coordinator.data.user.party.id + + if ( + party is not None and (party not in hass.data[HABITICA_KEY]) + ) or party != party_updated: + if party_added_by_this_entry: + config_entry.async_create_task( + hass, shutdown_party_coordinator(hass, party_added_by_this_entry) + ) + party_added_by_this_entry = None + if party: + identifier = {(DOMAIN, f"{config_entry.unique_id}_{party!s}")} + if device := device_reg.async_get_device(identifiers=identifier): + device_reg.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) + + hass.config_entries.async_schedule_reload(config_entry.entry_id) + + coordinator.async_add_listener(_party_update_listener) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True +async def shutdown_party_coordinator(hass: HomeAssistant, party_added: UUID) -> None: + """Handle party coordinator shutdown.""" + await hass.data[HABITICA_KEY][party_added].async_shutdown() + hass.data[HABITICA_KEY].pop(party_added) + + async def async_unload_entry(hass: HomeAssistant, entry: HabiticaConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/habitica/binary_sensor.py b/homeassistant/components/habitica/binary_sensor.py index c6f7ee0fb83..621c659a10c 100644 --- a/homeassistant/components/habitica/binary_sensor.py +++ b/homeassistant/components/habitica/binary_sensor.py @@ -6,18 +6,20 @@ from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum -from habiticalib import UserData +from habiticalib import ContentData, UserData from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from . import HABITICA_KEY from .const import ASSETS_URL -from .coordinator import HabiticaConfigEntry -from .entity import HabiticaBase +from .coordinator import HabiticaConfigEntry, HabiticaPartyCoordinator +from .entity import HabiticaBase, HabiticaPartyBase PARALLEL_UPDATES = 1 @@ -34,6 +36,7 @@ class HabiticaBinarySensor(StrEnum): """Habitica Entities.""" PENDING_QUEST = "pending_quest" + QUEST_RUNNING = "quest_running" def get_scroll_image_for_pending_quest_invitation(user: UserData) -> str | None: @@ -62,10 +65,21 @@ async def async_setup_entry( coordinator = config_entry.runtime_data - async_add_entities( + entities: list[BinarySensorEntity] = [ HabiticaBinarySensorEntity(coordinator, description) for description in BINARY_SENSOR_DESCRIPTIONS - ) + ] + + if party := coordinator.data.user.party.id: + party_coordinator = hass.data[HABITICA_KEY][party] + entities.append( + HabiticaPartyBinarySensorEntity( + party_coordinator, + config_entry, + coordinator.content, + ) + ) + async_add_entities(entities) class HabiticaBinarySensorEntity(HabiticaBase, BinarySensorEntity): @@ -86,3 +100,27 @@ class HabiticaBinarySensorEntity(HabiticaBase, BinarySensorEntity): ): return f"{ASSETS_URL}{entity_picture}" return None + + +class HabiticaPartyBinarySensorEntity(HabiticaPartyBase, BinarySensorEntity): + """Representation of a Habitica party binary sensor.""" + + entity_description = BinarySensorEntityDescription( + key=HabiticaBinarySensor.QUEST_RUNNING, + translation_key=HabiticaBinarySensor.QUEST_RUNNING, + device_class=BinarySensorDeviceClass.RUNNING, + ) + + def __init__( + self, + coordinator: HabiticaPartyCoordinator, + config_entry: HabiticaConfigEntry, + content: ContentData, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator, config_entry, self.entity_description, content) + + @property + def is_on(self) -> bool | None: + """If the binary sensor is on.""" + return self.coordinator.data.quest.active diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index 0e0a2db8d58..d9376820b16 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta @@ -13,6 +14,7 @@ from aiohttp import ClientError from habiticalib import ( Avatar, ContentData, + GroupData, Habitica, HabiticaException, NotAuthorizedError, @@ -49,10 +51,11 @@ class HabiticaData: type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator] -class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): - """Habitica Data Update Coordinator.""" +class HabiticaBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): + """Habitica coordinator base class.""" config_entry: HabiticaConfigEntry + _update_interval: timedelta def __init__( self, hass: HomeAssistant, config_entry: HabiticaConfigEntry, habitica: Habitica @@ -63,7 +66,7 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): _LOGGER, config_entry=config_entry, name=DOMAIN, - update_interval=timedelta(seconds=60), + update_interval=self._update_interval, request_refresh_debouncer=Debouncer( hass, _LOGGER, @@ -71,8 +74,40 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): immediate=False, ), ) + self.habitica = habitica - self.content: ContentData + + @abstractmethod + async def _update_data(self) -> _DataT: + """Fetch data.""" + + async def _async_update_data(self) -> _DataT: + """Fetch the latest party data.""" + + try: + return await self._update_data() + except TooManyRequestsError: + _LOGGER.debug("Rate limit exceeded, will try again later") + return self.data + except HabiticaException as e: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="service_call_exception", + translation_placeholders={"reason": str(e.error.message)}, + ) from e + except ClientError as e: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="service_call_exception", + translation_placeholders={"reason": str(e)}, + ) from e + + +class HabiticaDataUpdateCoordinator(HabiticaBaseCoordinator[HabiticaData]): + """Habitica Data Update Coordinator.""" + + _update_interval = timedelta(seconds=30) + content: ContentData async def _async_setup(self) -> None: """Set up Habitica integration.""" @@ -106,30 +141,16 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): translation_placeholders={"reason": str(e)}, ) from e - async def _async_update_data(self) -> HabiticaData: - try: - user = (await self.habitica.get_user()).data - tasks = (await self.habitica.get_tasks()).data - completed_todos = ( - await self.habitica.get_tasks(TaskFilter.COMPLETED_TODOS) - ).data - except TooManyRequestsError: - _LOGGER.debug("Rate limit exceeded, will try again later") - return self.data - except HabiticaException as e: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="service_call_exception", - translation_placeholders={"reason": str(e.error.message)}, - ) from e - except ClientError as e: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="service_call_exception", - translation_placeholders={"reason": str(e)}, - ) from e - else: - return HabiticaData(user=user, tasks=tasks + completed_todos) + async def _update_data(self) -> HabiticaData: + """Fetch the latest data.""" + + user = (await self.habitica.get_user()).data + tasks = (await self.habitica.get_tasks()).data + completed_todos = ( + await self.habitica.get_tasks(TaskFilter.COMPLETED_TODOS) + ).data + + return HabiticaData(user=user, tasks=tasks + completed_todos) async def execute(self, func: Callable[[Habitica], Any]) -> None: """Execute an API call.""" @@ -169,3 +190,13 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): await self.habitica.generate_avatar(fp=png, avatar=avatar, fmt="PNG") return png.getvalue() + + +class HabiticaPartyCoordinator(HabiticaBaseCoordinator[GroupData]): + """Habitica Party Coordinator.""" + + _update_interval = timedelta(minutes=15) + + async def _update_data(self) -> GroupData: + """Fetch the latest party data.""" + return (await self.habitica.get_group()).data diff --git a/homeassistant/components/habitica/entity.py b/homeassistant/components/habitica/entity.py index 6d320f93517..fa227fec334 100644 --- a/homeassistant/components/habitica/entity.py +++ b/homeassistant/components/habitica/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +from habiticalib import ContentData from yarl import URL from homeassistant.const import CONF_URL @@ -12,7 +13,11 @@ from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER, NAME -from .coordinator import HabiticaDataUpdateCoordinator +from .coordinator import ( + HabiticaConfigEntry, + HabiticaDataUpdateCoordinator, + HabiticaPartyCoordinator, +) class HabiticaBase(CoordinatorEntity[HabiticaDataUpdateCoordinator]): @@ -45,3 +50,33 @@ class HabiticaBase(CoordinatorEntity[HabiticaDataUpdateCoordinator]): ), identifiers={(DOMAIN, coordinator.config_entry.unique_id)}, ) + + +class HabiticaPartyBase(CoordinatorEntity[HabiticaPartyCoordinator]): + """Base Habitica entity representing a party.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: HabiticaPartyCoordinator, + config_entry: HabiticaConfigEntry, + entity_description: EntityDescription, + content: ContentData, + ) -> None: + """Initialize a Habitica party entity.""" + super().__init__(coordinator) + if TYPE_CHECKING: + assert config_entry.unique_id + unique_id = f"{config_entry.unique_id}_{coordinator.data.id!s}" + self.entity_description = entity_description + self._attr_unique_id = f"{unique_id}_{entity_description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=NAME, + name=coordinator.data.summary, + identifiers={(DOMAIN, unique_id)}, + via_device=(DOMAIN, config_entry.unique_id), + ) + self.content = content diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json index be25bebe779..0b5d4aaa682 100644 --- a/homeassistant/components/habitica/icons.json +++ b/homeassistant/components/habitica/icons.json @@ -156,6 +156,24 @@ }, "pending_quest_items": { "default": "mdi:sack" + }, + "group_leader": { + "default": "mdi:shield-crown" + }, + "quest": { + "default": "mdi:script-text-outline" + }, + "boss": { + "default": "mdi:emoticon-devil" + }, + "boss_hp": { + "default": "mdi:heart" + }, + "boss_hp_remaining": { + "default": "mdi:heart" + }, + "collected_items": { + "default": "mdi:sack" } }, "switch": { @@ -172,6 +190,9 @@ "state": { "on": "mdi:script-text-outline" } + }, + "quest_running": { + "default": "mdi:script-text-play" } } }, diff --git a/homeassistant/components/habitica/image.py b/homeassistant/components/habitica/image.py index 1669f124bc7..f064074ea0a 100644 --- a/homeassistant/components/habitica/image.py +++ b/homeassistant/components/habitica/image.py @@ -4,15 +4,21 @@ from __future__ import annotations from enum import StrEnum -from habiticalib import Avatar, extract_avatar +from habiticalib import Avatar, ContentData, extract_avatar -from homeassistant.components.image import ImageEntity, ImageEntityDescription +from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from .coordinator import HabiticaConfigEntry, HabiticaDataUpdateCoordinator -from .entity import HabiticaBase +from . import HABITICA_KEY +from .const import ASSETS_URL +from .coordinator import ( + HabiticaConfigEntry, + HabiticaDataUpdateCoordinator, + HabiticaPartyCoordinator, +) +from .entity import HabiticaBase, HabiticaPartyBase PARALLEL_UPDATES = 1 @@ -21,6 +27,7 @@ class HabiticaImageEntity(StrEnum): """Image entities.""" AVATAR = "avatar" + QUEST_IMAGE = "quest_image" async def async_setup_entry( @@ -31,8 +38,17 @@ async def async_setup_entry( """Set up the habitica image platform.""" coordinator = config_entry.runtime_data + entities: list[ImageEntity] = [HabiticaImage(hass, coordinator)] - async_add_entities([HabiticaImage(hass, coordinator)]) + if party := coordinator.data.user.party.id: + party_coordinator = hass.data[HABITICA_KEY][party] + entities.append( + HabiticaPartyImage( + hass, party_coordinator, config_entry, coordinator.content + ) + ) + + async_add_entities(entities) class HabiticaImage(HabiticaBase, ImageEntity): @@ -72,3 +88,58 @@ class HabiticaImage(HabiticaBase, ImageEntity): if not self._cache and self._avatar: self._cache = await self.coordinator.generate_avatar(self._avatar) return self._cache + + +class HabiticaPartyImage(HabiticaPartyBase, ImageEntity): + """A Habitica image entity of a party.""" + + entity_description = ImageEntityDescription( + key=HabiticaImageEntity.QUEST_IMAGE, + translation_key=HabiticaImageEntity.QUEST_IMAGE, + ) + _attr_content_type = "image/png" + + def __init__( + self, + hass: HomeAssistant, + coordinator: HabiticaPartyCoordinator, + config_entry: HabiticaConfigEntry, + content: ContentData, + ) -> None: + """Initialize the image entity.""" + super().__init__(coordinator, config_entry, self.entity_description, content) + ImageEntity.__init__(self, hass) + + self._attr_image_url = self.image_url + self._attr_image_last_updated = dt_util.utcnow() + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + if self.image_url != self._attr_image_url: + self._attr_image_url = self.image_url + self._cached_image = None + self._attr_image_last_updated = dt_util.utcnow() + + super()._handle_coordinator_update() + + @property + def image_url(self) -> str | None: + """Return URL of image.""" + return ( + f"{ASSETS_URL}quest_{key}.png" + if (key := self.coordinator.data.quest.key) + else None + ) + + async def _async_load_image_from_url(self, url: str) -> Image | None: + """Load an image by url. + + AWS sometimes returns 'application/octet-stream' as content-type + """ + if response := await self._fetch_url(url): + return Image( + content=response.content, + content_type=self._attr_content_type, + ) + return None diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index d890ed23676..99c84f9686f 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["habiticalib"], "quality_scale": "platinum", - "requirements": ["habiticalib==0.4.1"] + "requirements": ["habiticalib==0.4.3"] } diff --git a/homeassistant/components/habitica/quality_scale.yaml b/homeassistant/components/habitica/quality_scale.yaml index 1752e67cf46..c5131b81a4d 100644 --- a/homeassistant/components/habitica/quality_scale.yaml +++ b/homeassistant/components/habitica/quality_scale.yaml @@ -72,7 +72,7 @@ rules: comment: Used to inform of deprecated entities and actions. stale-devices: status: done - comment: Not applicable. Only one device per config entry. Removed together with the config entry. + comment: Party device is remove if stale. # Platinum async-dependency: done diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 6d077495c4f..7a84d589bfb 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -8,7 +8,7 @@ from enum import StrEnum import logging from typing import Any -from habiticalib import ContentData, HabiticaClass, TaskData, UserData, ha +from habiticalib import ContentData, GroupData, HabiticaClass, TaskData, UserData, ha from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,15 +20,19 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util +from . import HABITICA_KEY from .const import ASSETS_URL from .coordinator import HabiticaConfigEntry -from .entity import HabiticaBase +from .entity import HabiticaBase, HabiticaPartyBase from .util import ( + collected_quest_items, get_attribute_points, get_attributes_total, inventory_list, pending_damage, pending_quest_items, + quest_attributes, + quest_boss, ) _LOGGER = logging.getLogger(__name__) @@ -55,6 +59,17 @@ class HabiticaSensorEntityDescription(SensorEntityDescription): entity_picture: str | None = None +@dataclass(kw_only=True, frozen=True) +class HabiticaPartySensorEntityDescription(SensorEntityDescription): + """Habitica Party Sensor Description.""" + + value_fn: Callable[[GroupData, ContentData], StateType] + entity_picture: Callable[[GroupData], str | None] | str | None = None + attributes_fn: Callable[[GroupData, ContentData], dict[str, Any] | None] | None = ( + None + ) + + @dataclass(kw_only=True, frozen=True) class HabiticaTaskSensorEntityDescription(SensorEntityDescription): """Habitica Task Sensor Description.""" @@ -89,6 +104,13 @@ class HabiticaSensorEntity(StrEnum): QUEST_SCROLLS = "quest_scrolls" PENDING_DAMAGE = "pending_damage" PENDING_QUEST_ITEMS = "pending_quest_items" + MEMBER_COUNT = "member_count" + GROUP_LEADER = "group_leader" + QUEST = "quest" + BOSS = "boss" + BOSS_HP = "boss_hp" + BOSS_HP_REMAINING = "boss_hp_remaining" + COLLECTED_ITEMS = "collected_items" SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = ( @@ -262,6 +284,67 @@ SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = ( ) +SENSOR_DESCRIPTIONS_PARTY: tuple[HabiticaPartySensorEntityDescription, ...] = ( + HabiticaPartySensorEntityDescription( + key=HabiticaSensorEntity.MEMBER_COUNT, + translation_key=HabiticaSensorEntity.MEMBER_COUNT, + value_fn=lambda party, _: party.memberCount, + entity_picture=ha.PARTY, + ), + HabiticaPartySensorEntityDescription( + key=HabiticaSensorEntity.GROUP_LEADER, + translation_key=HabiticaSensorEntity.GROUP_LEADER, + value_fn=lambda party, _: party.leader.profile.name, + ), + HabiticaPartySensorEntityDescription( + key=HabiticaSensorEntity.QUEST, + translation_key=HabiticaSensorEntity.QUEST, + value_fn=lambda p, c: c.quests[p.quest.key].text if p.quest.key else None, + attributes_fn=quest_attributes, + entity_picture=( + lambda party: f"inventory_quest_scroll_{party.quest.key}.png" + if party.quest.key + else None + ), + ), + HabiticaPartySensorEntityDescription( + key=HabiticaSensorEntity.BOSS, + translation_key=HabiticaSensorEntity.BOSS, + value_fn=lambda p, c: boss.name if (boss := quest_boss(p, c)) else None, + ), + HabiticaPartySensorEntityDescription( + key=HabiticaSensorEntity.BOSS_HP, + translation_key=HabiticaSensorEntity.BOSS_HP, + value_fn=lambda p, c: boss.hp if (boss := quest_boss(p, c)) else None, + entity_picture=ha.HP, + suggested_display_precision=0, + ), + HabiticaPartySensorEntityDescription( + key=HabiticaSensorEntity.BOSS_HP_REMAINING, + translation_key=HabiticaSensorEntity.BOSS_HP_REMAINING, + value_fn=lambda p, _: p.quest.progress.hp, + entity_picture=ha.HP, + suggested_display_precision=2, + ), + HabiticaPartySensorEntityDescription( + key=HabiticaSensorEntity.COLLECTED_ITEMS, + translation_key=HabiticaSensorEntity.COLLECTED_ITEMS, + value_fn=( + lambda p, _: sum(n for n in p.quest.progress.collect.values()) + if p.quest.progress.collect + else None + ), + attributes_fn=collected_quest_items, + entity_picture=( + lambda p: f"quest_{p.quest.key}_{k}.png" + if p.quest.progress.collect + and (k := next(iter(p.quest.progress.collect), None)) + else None + ), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: HabiticaConfigEntry, @@ -275,6 +358,18 @@ async def async_setup_entry( HabiticaSensor(coordinator, description) for description in SENSOR_DESCRIPTIONS ) + if party := coordinator.data.user.party.id: + party_coordinator = hass.data[HABITICA_KEY][party] + async_add_entities( + HabiticaPartySensor( + party_coordinator, + config_entry, + description, + coordinator.content, + ) + for description in SENSOR_DESCRIPTIONS_PARTY + ) + class HabiticaSensor(HabiticaBase, SensorEntity): """A generic Habitica sensor.""" @@ -317,3 +412,39 @@ class HabiticaSensor(HabiticaBase, SensorEntity): ) return None + + +class HabiticaPartySensor(HabiticaPartyBase, SensorEntity): + """Habitica party sensor.""" + + entity_description: HabiticaPartySensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return the state of the device.""" + + return self.entity_description.value_fn(self.coordinator.data, self.content) + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend, if any.""" + pic = self.entity_description.entity_picture + + entity_picture = ( + pic if isinstance(pic, str) or pic is None else pic(self.coordinator.data) + ) + + return ( + None + if not entity_picture + else entity_picture + if entity_picture.startswith("data:image") + else f"{ASSETS_URL}{entity_picture}" + ) + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return entity specific state attributes.""" + if func := self.entity_description.attributes_fn: + return func(self.coordinator.data, self.content) + return None diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 6f0b3dc35cd..1d62b242149 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -7,6 +7,7 @@ "unit_health_points": "HP", "unit_mana_points": "MP", "unit_experience_points": "XP", + "unit_items": "items", "config_entry_description": "Select the Habitica account to update a task.", "task_description": "The name (or task ID) of the task you want to update.", "rename_name": "Rename", @@ -63,7 +64,8 @@ "repeat_weekly_options_name": "Weekly repeat days", "repeat_weekly_options_description": "Options related to weekly repetition, applicable when the repetition interval is set to weekly.", "repeat_monthly_options_name": "Monthly repeat day", - "repeat_monthly_options_description": "Options related to monthly repetition, applicable when the repetition interval is set to monthly." + "repeat_monthly_options_description": "Options related to monthly repetition, applicable when the repetition interval is set to monthly.", + "quest_name": "Quest" }, "config": { "abort": { @@ -173,6 +175,9 @@ "binary_sensor": { "pending_quest": { "name": "Pending quest invitation" + }, + "quest_running": { + "name": "Quest status" } }, "button": { @@ -251,6 +256,9 @@ "image": { "avatar": { "name": "Avatar" + }, + "quest_image": { + "name": "[%key:component::habitica::common::quest_name%]" } }, "sensor": { @@ -420,7 +428,37 @@ }, "pending_quest_items": { "name": "Pending quest items", - "unit_of_measurement": "items" + "unit_of_measurement": "[%key:component::habitica::common::unit_items%]" + }, + "member_count": { + "name": "Member count", + "unit_of_measurement": "members" + }, + "group_leader": { + "name": "Group leader" + }, + "quest": { + "name": "[%key:component::habitica::common::quest_name%]", + "state_attributes": { + "quest_details": { + "name": "Quest details" + } + } + }, + "boss": { + "name": "Quest boss" + }, + "boss_hp": { + "name": "Boss health", + "unit_of_measurement": "[%key:component::habitica::common::unit_health_points%]" + }, + "boss_hp_remaining": { + "name": "Boss health remaining", + "unit_of_measurement": "[%key:component::habitica::common::unit_health_points%]" + }, + "collected_items": { + "name": "Collected quest items", + "unit_of_measurement": "[%key:component::habitica::common::unit_items%]" } }, "switch": { diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index 4f948b9b4d2..8c2148192a3 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import asdict, fields import datetime from math import floor -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal from dateutil.rrule import ( DAILY, @@ -21,7 +21,7 @@ from dateutil.rrule import ( YEARLY, rrule, ) -from habiticalib import ContentData, Frequency, TaskData, UserData +from habiticalib import ContentData, Frequency, GroupData, QuestBoss, TaskData, UserData from homeassistant.util import dt as dt_util @@ -184,3 +184,32 @@ def pending_damage(user: UserData, content: ContentData) -> float | None: and content.quests[user.party.quest.key].boss is not None else None ) + + +def quest_attributes(party: GroupData, content: ContentData) -> dict[str, Any]: + """Quest description.""" + return { + "quest_details": content.quests[party.quest.key].notes + if party.quest.key + else None, + "quest_participants": f"{sum(x is True for x in party.quest.members.values())} / {party.memberCount}", + } + + +def quest_boss(party: GroupData, content: ContentData) -> QuestBoss | None: + """Quest boss.""" + + return content.quests[party.quest.key].boss if party.quest.key else None + + +def collected_quest_items(party: GroupData, content: ContentData) -> dict[str, Any]: + """List collected quest items.""" + + return ( + { + collect[k].text: f"{v} / {collect[k].count}" + for k, v in party.quest.progress.collect.items() + } + if party.quest.key and (collect := content.quests[party.quest.key].collect) + else {} + ) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 35f7f48481e..22406e86ba1 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -61,18 +61,19 @@ PLACEHOLDER_KEY_REASON = "reason" UNSUPPORTED_REASONS = { "apparmor", + "cgroup_version", "connectivity_check", "content_trust", "dbus", "dns_server", "docker_configuration", "docker_version", - "cgroup_version", "job_conditions", "lxc", "network_manager", "os", "os_agent", + "os_version", "restart_policy", "software", "source_mods", @@ -80,6 +81,7 @@ UNSUPPORTED_REASONS = { "systemd", "systemd_journal", "systemd_resolved", + "virtualization_image", } # Some unsupported reasons also mark the system as unhealthy. If the unsupported reason # provides no additional information beyond the unhealthy one then skip that repair. @@ -103,6 +105,7 @@ ISSUE_KEYS_FOR_REPAIRS = { ISSUE_KEY_SYSTEM_DOCKER_CONFIG, ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING, ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED, + "issue_system_disk_lifetime", } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index a2af6fb217c..34a8f466158 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/hassio", "iot_class": "local_polling", "quality_scale": "internal", - "requirements": ["aiohasupervisor==0.3.1"], + "requirements": ["aiohasupervisor==0.3.2b0"], "single_config_entry": true } diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 5df197bddcb..393fe480057 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -115,6 +115,10 @@ } } }, + "issue_system_disk_lifetime": { + "title": "Disk lifetime exceeding 90%", + "description": "The data disk has exceeded 90% of its expected lifespan. The disk may soon malfunction which can lead to data loss. You should replace it soon and migrate your data." + }, "unhealthy": { "title": "Unhealthy system - {reason}", "description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more." diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index dde50da1af3..5ea0d217f14 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.78", "babel==2.15.0"] + "requirements": ["holidays==0.79", "babel==2.15.0"] } diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 64bf4af29a4..c1aea03134f 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -61,19 +61,12 @@ BSH_DOOR_STATE_LOCKED = "BSH.Common.EnumType.DoorState.Locked" BSH_DOOR_STATE_OPEN = "BSH.Common.EnumType.DoorState.Open" -SERVICE_OPTION_ACTIVE = "set_option_active" -SERVICE_OPTION_SELECTED = "set_option_selected" -SERVICE_PAUSE_PROGRAM = "pause_program" -SERVICE_RESUME_PROGRAM = "resume_program" -SERVICE_SELECT_PROGRAM = "select_program" SERVICE_SET_PROGRAM_AND_OPTIONS = "set_program_and_options" SERVICE_SETTING = "change_setting" -SERVICE_START_PROGRAM = "start_program" ATTR_AFFECTS_TO = "affects_to" ATTR_KEY = "key" ATTR_PROGRAM = "program" -ATTR_UNIT = "unit" ATTR_VALUE = "value" AFFECTS_TO_ACTIVE_PROGRAM = "active_program" diff --git a/homeassistant/components/home_connect/services.py b/homeassistant/components/home_connect/services.py index 09c2f4a967d..ca6eca4d919 100644 --- a/homeassistant/components/home_connect/services.py +++ b/homeassistant/components/home_connect/services.py @@ -8,7 +8,6 @@ from typing import Any, cast from aiohomeconnect.client import Client as HomeConnectClient from aiohomeconnect.model import ( ArrayOfOptions, - CommandKey, Option, OptionKey, ProgramKey, @@ -21,7 +20,6 @@ from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import ( AFFECTS_TO_ACTIVE_PROGRAM, @@ -29,18 +27,11 @@ from .const import ( ATTR_AFFECTS_TO, ATTR_KEY, ATTR_PROGRAM, - ATTR_UNIT, ATTR_VALUE, DOMAIN, PROGRAM_ENUM_OPTIONS, - SERVICE_OPTION_ACTIVE, - SERVICE_OPTION_SELECTED, - SERVICE_PAUSE_PROGRAM, - SERVICE_RESUME_PROGRAM, - SERVICE_SELECT_PROGRAM, SERVICE_SET_PROGRAM_AND_OPTIONS, SERVICE_SETTING, - SERVICE_START_PROGRAM, TRANSLATION_KEYS_PROGRAMS_MAP, ) from .coordinator import HomeConnectConfigEntry @@ -88,43 +79,6 @@ SERVICE_SETTING_SCHEMA = vol.Schema( } ) -# DEPRECATED: Remove in 2025.9.0 -SERVICE_OPTION_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): str, - vol.Required(ATTR_KEY): vol.All( - vol.Coerce(OptionKey), - vol.NotIn([OptionKey.UNKNOWN]), - ), - vol.Required(ATTR_VALUE): vol.Any(str, int, bool), - vol.Optional(ATTR_UNIT): str, - } -) - -# DEPRECATED: Remove in 2025.9.0 -SERVICE_PROGRAM_SCHEMA = vol.Any( - { - vol.Required(ATTR_DEVICE_ID): str, - vol.Required(ATTR_PROGRAM): vol.All( - vol.Coerce(ProgramKey), - vol.NotIn([ProgramKey.UNKNOWN]), - ), - vol.Required(ATTR_KEY): vol.All( - vol.Coerce(OptionKey), - vol.NotIn([OptionKey.UNKNOWN]), - ), - vol.Required(ATTR_VALUE): vol.Any(int, str), - vol.Optional(ATTR_UNIT): str, - }, - { - vol.Required(ATTR_DEVICE_ID): str, - vol.Required(ATTR_PROGRAM): vol.All( - vol.Coerce(ProgramKey), - vol.NotIn([ProgramKey.UNKNOWN]), - ), - }, -) - def _require_program_or_at_least_one_option(data: dict) -> dict: if ATTR_PROGRAM not in data and not any( @@ -216,205 +170,6 @@ async def _get_client_and_ha_id( return entry.runtime_data.client, ha_id -async def _async_service_program(call: ServiceCall, start: bool) -> None: - """Execute calls to services taking a program.""" - program = call.data[ATTR_PROGRAM] - client, ha_id = await _get_client_and_ha_id(call.hass, call.data[ATTR_DEVICE_ID]) - - option_key = call.data.get(ATTR_KEY) - options = ( - [ - Option( - option_key, - call.data[ATTR_VALUE], - unit=call.data.get(ATTR_UNIT), - ) - ] - if option_key is not None - else None - ) - - async_create_issue( - call.hass, - DOMAIN, - "deprecated_set_program_and_option_actions", - breaks_in_ha_version="2025.9.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_set_program_and_option_actions", - translation_placeholders={ - "new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS, - "remove_release": "2025.9.0", - "deprecated_action_yaml": "\n".join( - [ - "```yaml", - f"action: {DOMAIN}.{SERVICE_START_PROGRAM if start else SERVICE_SELECT_PROGRAM}", - "data:", - f" {ATTR_DEVICE_ID}: DEVICE_ID", - f" {ATTR_PROGRAM}: {program}", - *([f" {ATTR_KEY}: {options[0].key}"] if options else []), - *([f" {ATTR_VALUE}: {options[0].value}"] if options else []), - *( - [f" {ATTR_UNIT}: {options[0].unit}"] - if options and options[0].unit - else [] - ), - "```", - ] - ), - "new_action_yaml": "\n ".join( - [ - "```yaml", - f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}", - "data:", - f" {ATTR_DEVICE_ID}: DEVICE_ID", - f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if start else AFFECTS_TO_SELECTED_PROGRAM}", - f" {ATTR_PROGRAM}: {bsh_key_to_translation_key(program.value)}", - *( - [ - f" {bsh_key_to_translation_key(options[0].key)}: {options[0].value}" - ] - if options - else [] - ), - "```", - ] - ), - "repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)", - }, - ) - - try: - if start: - await client.start_program(ha_id, program_key=program, options=options) - else: - await client.set_selected_program( - ha_id, program_key=program, options=options - ) - except HomeConnectError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="start_program" if start else "select_program", - translation_placeholders={ - **get_dict_from_home_connect_error(err), - "program": program, - }, - ) from err - - -async def _async_service_set_program_options(call: ServiceCall, active: bool) -> None: - """Execute calls to services taking a program.""" - option_key = call.data[ATTR_KEY] - value = call.data[ATTR_VALUE] - unit = call.data.get(ATTR_UNIT) - client, ha_id = await _get_client_and_ha_id(call.hass, call.data[ATTR_DEVICE_ID]) - - async_create_issue( - call.hass, - DOMAIN, - "deprecated_set_program_and_option_actions", - breaks_in_ha_version="2025.9.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_set_program_and_option_actions", - translation_placeholders={ - "new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS, - "remove_release": "2025.9.0", - "deprecated_action_yaml": "\n".join( - [ - "```yaml", - f"action: {DOMAIN}.{SERVICE_OPTION_ACTIVE if active else SERVICE_OPTION_SELECTED}", - "data:", - f" {ATTR_DEVICE_ID}: DEVICE_ID", - f" {ATTR_KEY}: {option_key}", - f" {ATTR_VALUE}: {value}", - *([f" {ATTR_UNIT}: {unit}"] if unit else []), - "```", - ] - ), - "new_action_yaml": "\n ".join( - [ - "```yaml", - f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}", - "data:", - f" {ATTR_DEVICE_ID}: DEVICE_ID", - f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if active else AFFECTS_TO_SELECTED_PROGRAM}", - f" {bsh_key_to_translation_key(option_key)}: {value}", - "```", - ] - ), - "repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)", - }, - ) - try: - if active: - await client.set_active_program_option( - ha_id, - option_key=option_key, - value=value, - unit=unit, - ) - else: - await client.set_selected_program_option( - ha_id, - option_key=option_key, - value=value, - unit=unit, - ) - except HomeConnectError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="set_options_active_program" - if active - else "set_options_selected_program", - translation_placeholders={ - **get_dict_from_home_connect_error(err), - "key": option_key, - "value": str(value), - }, - ) from err - - -async def _async_service_command(call: ServiceCall, command_key: CommandKey) -> None: - """Execute calls to services executing a command.""" - client, ha_id = await _get_client_and_ha_id(call.hass, call.data[ATTR_DEVICE_ID]) - - async_create_issue( - call.hass, - DOMAIN, - "deprecated_command_actions", - breaks_in_ha_version="2025.9.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_command_actions", - ) - - try: - await client.put_command(ha_id, command_key=command_key, value=True) - except HomeConnectError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="execute_command", - translation_placeholders={ - **get_dict_from_home_connect_error(err), - "command": command_key.value, - }, - ) from err - - -async def async_service_option_active(call: ServiceCall) -> None: - """Service for setting an option for an active program.""" - await _async_service_set_program_options(call, True) - - -async def async_service_option_selected(call: ServiceCall) -> None: - """Service for setting an option for a selected program.""" - await _async_service_set_program_options(call, False) - - async def async_service_setting(call: ServiceCall) -> None: """Service for changing a setting.""" key = call.data[ATTR_KEY] @@ -435,21 +190,6 @@ async def async_service_setting(call: ServiceCall) -> None: ) from err -async def async_service_pause_program(call: ServiceCall) -> None: - """Service for pausing a program.""" - await _async_service_command(call, CommandKey.BSH_COMMON_PAUSE_PROGRAM) - - -async def async_service_resume_program(call: ServiceCall) -> None: - """Service for resuming a paused program.""" - await _async_service_command(call, CommandKey.BSH_COMMON_RESUME_PROGRAM) - - -async def async_service_select_program(call: ServiceCall) -> None: - """Service for selecting a program.""" - await _async_service_program(call, False) - - async def async_service_set_program_and_options(call: ServiceCall) -> None: """Service for setting a program and options.""" data = dict(call.data) @@ -517,54 +257,13 @@ async def async_service_set_program_and_options(call: ServiceCall) -> None: ) from err -async def async_service_start_program(call: ServiceCall) -> None: - """Service for starting a program.""" - await _async_service_program(call, True) - - @callback def async_setup_services(hass: HomeAssistant) -> None: """Register custom actions.""" - hass.services.async_register( - DOMAIN, - SERVICE_OPTION_ACTIVE, - async_service_option_active, - schema=SERVICE_OPTION_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_OPTION_SELECTED, - async_service_option_selected, - schema=SERVICE_OPTION_SCHEMA, - ) hass.services.async_register( DOMAIN, SERVICE_SETTING, async_service_setting, schema=SERVICE_SETTING_SCHEMA ) - hass.services.async_register( - DOMAIN, - SERVICE_PAUSE_PROGRAM, - async_service_pause_program, - schema=SERVICE_COMMAND_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_RESUME_PROGRAM, - async_service_resume_program, - schema=SERVICE_COMMAND_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_SELECT_PROGRAM, - async_service_select_program, - schema=SERVICE_PROGRAM_SCHEMA, - ) - hass.services.async_register( - DOMAIN, - SERVICE_START_PROGRAM, - async_service_start_program, - schema=SERVICE_PROGRAM_SCHEMA, - ) hass.services.async_register( DOMAIN, SERVICE_SET_PROGRAM_AND_OPTIONS, diff --git a/homeassistant/components/home_connect/services.yaml b/homeassistant/components/home_connect/services.yaml index e07e8e91457..a1e9d121704 100644 --- a/homeassistant/components/home_connect/services.yaml +++ b/homeassistant/components/home_connect/services.yaml @@ -1,51 +1,3 @@ -start_program: - fields: - device_id: - required: true - selector: - device: - integration: home_connect - program: - example: "Dishcare.Dishwasher.Program.Auto2" - required: true - selector: - text: - key: - example: "BSH.Common.Option.StartInRelative" - selector: - text: - value: - example: 1800 - selector: - object: - unit: - example: "seconds" - selector: - text: -select_program: - fields: - device_id: - required: true - selector: - device: - integration: home_connect - program: - example: "Dishcare.Dishwasher.Program.Auto2" - required: true - selector: - text: - key: - example: "BSH.Common.Option.StartInRelative" - selector: - text: - value: - example: 1800 - selector: - object: - unit: - example: "seconds" - selector: - text: set_program_and_options: fields: device_id: @@ -599,54 +551,6 @@ set_program_and_options: - laundry_care_common_enum_type_vario_perfect_off - laundry_care_common_enum_type_vario_perfect_eco_perfect - laundry_care_common_enum_type_vario_perfect_speed_perfect -pause_program: - fields: - device_id: - required: true - selector: - device: - integration: home_connect -resume_program: - fields: - device_id: - required: true - selector: - device: - integration: home_connect -set_option_active: - fields: - device_id: - required: true - selector: - device: - integration: home_connect - key: - example: "LaundryCare.Dryer.Option.DryingTarget" - required: true - selector: - text: - value: - example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" - required: true - selector: - object: -set_option_selected: - fields: - device_id: - required: true - selector: - device: - integration: home_connect - key: - example: "LaundryCare.Dryer.Option.DryingTarget" - required: true - selector: - text: - value: - example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" - required: true - selector: - object: change_setting: fields: device_id: diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index fa24177a967..1d3bffb7847 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -145,28 +145,6 @@ } } } - }, - "deprecated_command_actions": { - "title": "The command related actions are deprecated in favor of the new buttons", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::home_connect::issues::deprecated_command_actions::title%]", - "description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue." - } - } - } - }, - "deprecated_set_program_and_option_actions": { - "title": "The executed action is deprecated", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::home_connect::issues::deprecated_set_program_and_option_actions::title%]", - "description": "`start_program`, `select_program`, `set_option_active`, and `set_option_selected` actions are deprecated and will be removed in the {remove_release} release, please use the `{new_action_key}` action instead. For the executed action:\n{deprecated_action_yaml}\nyou can do the following transformation using the recognized options:\n {new_action_yaml}\nIf the option is not in the recognized options, please submit an issue or a pull request requesting the addition of the option at {repo_link}." - } - } - } } }, "selector": { @@ -517,49 +495,6 @@ } }, "services": { - "start_program": { - "name": "Start program", - "description": "Selects a program and starts it.", - "fields": { - "device_id": { - "name": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::name%]", - "description": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::description%]" - }, - "program": { "name": "Program", "description": "Program to select." }, - "key": { "name": "Option key", "description": "Key of the option." }, - "value": { - "name": "Option value", - "description": "Value of the option." - }, - "unit": { "name": "Option unit", "description": "Unit for the option." } - } - }, - "select_program": { - "name": "Select program", - "description": "Selects a program without starting it.", - "fields": { - "device_id": { - "name": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::name%]", - "description": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::description%]" - }, - "program": { - "name": "[%key:component::home_connect::services::start_program::fields::program::name%]", - "description": "[%key:component::home_connect::services::start_program::fields::program::description%]" - }, - "key": { - "name": "[%key:component::home_connect::services::start_program::fields::key::name%]", - "description": "[%key:component::home_connect::services::start_program::fields::key::description%]" - }, - "value": { - "name": "[%key:component::home_connect::services::start_program::fields::value::name%]", - "description": "[%key:component::home_connect::services::start_program::fields::value::description%]" - }, - "unit": { - "name": "[%key:component::home_connect::services::start_program::fields::unit::name%]", - "description": "[%key:component::home_connect::services::start_program::fields::unit::description%]" - } - } - }, "set_program_and_options": { "name": "Set program and options", "description": "Starts or selects a program with options or sets the options for the active or the selected program.", @@ -744,62 +679,6 @@ } } }, - "pause_program": { - "name": "Pause program", - "description": "Pauses the current running program.", - "fields": { - "device_id": { - "name": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::name%]", - "description": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::description%]" - } - } - }, - "resume_program": { - "name": "Resume program", - "description": "Resumes a paused program.", - "fields": { - "device_id": { - "name": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::name%]", - "description": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::description%]" - } - } - }, - "set_option_active": { - "name": "Set active program option", - "description": "Sets an option for the active program.", - "fields": { - "device_id": { - "name": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::name%]", - "description": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::description%]" - }, - "key": { - "name": "Key", - "description": "[%key:component::home_connect::services::start_program::fields::key::description%]" - }, - "value": { - "name": "Value", - "description": "[%key:component::home_connect::services::start_program::fields::value::description%]" - } - } - }, - "set_option_selected": { - "name": "Set selected program option", - "description": "Sets options for the selected program.", - "fields": { - "device_id": { - "name": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::name%]", - "description": "[%key:component::home_connect::services::set_program_and_options::fields::device_id::description%]" - }, - "key": { - "name": "[%key:component::home_connect::services::start_program::fields::key::name%]", - "description": "[%key:component::home_connect::services::start_program::fields::key::description%]" - }, - "value": { - "name": "[%key:component::home_connect::services::start_program::fields::value::name%]", - "description": "[%key:component::home_connect::services::start_program::fields::value::description%]" - } - } - }, "change_setting": { "name": "Change setting", "description": "Changes a setting.", diff --git a/homeassistant/components/homeassistant_hardware/coordinator.py b/homeassistant/components/homeassistant_hardware/coordinator.py index 36a2f407282..6c4b2cb38e4 100644 --- a/homeassistant/components/homeassistant_hardware/coordinator.py +++ b/homeassistant/components/homeassistant_hardware/coordinator.py @@ -40,7 +40,6 @@ class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]): update_interval=FIRMWARE_REFRESH_INTERVAL, config_entry=config_entry, ) - self.hass = hass self.session = session self.client = FirmwareUpdateClient(url, session) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 48327910be6..9fef970d560 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -291,7 +291,7 @@ class NitrogenDioxideSensor(AirQualitySensor): class VolatileOrganicCompoundsSensor(AirQualitySensor): """Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor. - Sensor entity must return VOC in µg/m3. + Sensor entity must return VOC in μg/m3. """ def create_services(self) -> None: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index ea67e30a3c1..9a0a288fad4 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -494,7 +494,7 @@ def temperature_to_states(temperature: float, unit: str) -> float: def density_to_air_quality(density: float) -> int: - """Map PM2.5 µg/m3 density to HomeKit AirQuality level.""" + """Map PM2.5 μg/m3 density to HomeKit AirQuality level.""" if density <= 9: # US AQI 0-50 (HomeKit: Excellent) return 1 if density <= 35.4: # US AQI 51-100 (HomeKit: Good) @@ -507,7 +507,7 @@ def density_to_air_quality(density: float) -> int: def density_to_air_quality_pm10(density: float) -> int: - """Map PM10 µg/m3 density to HomeKit AirQuality level.""" + """Map PM10 μg/m3 density to HomeKit AirQuality level.""" if density <= 54: # US AQI 0-50 (HomeKit: Excellent) return 1 if density <= 154: # US AQI 51-100 (HomeKit: Good) @@ -520,7 +520,7 @@ def density_to_air_quality_pm10(density: float) -> int: def density_to_air_quality_nitrogen_dioxide(density: float) -> int: - """Map nitrogen dioxide µg/m3 to HomeKit AirQuality level.""" + """Map nitrogen dioxide μg/m3 to HomeKit AirQuality level.""" if density <= 30: return 1 if density <= 60: @@ -533,7 +533,7 @@ def density_to_air_quality_nitrogen_dioxide(density: float) -> int: def density_to_air_quality_voc(density: float) -> int: - """Map VOCs µg/m3 to HomeKit AirQuality level. + """Map VOCs μg/m3 to HomeKit AirQuality level. The VOC mappings use the IAQ guidelines for Europe released by the WHO (World Health Organization). Referenced from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 931bd40d64c..139ceef48ad 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -228,7 +228,9 @@ class HKDevice: _LOGGER.debug( "Called async_set_available_state with %s for %s", available, self.unique_id ) - if self.available == available: + # Don't mark entities as unavailable during shutdown to preserve their last known state + # Also skip if the availability state hasn't changed + if (self.hass.is_stopping and not available) or self.available == available: return self.available = available for callback_ in self._availability_callbacks: @@ -294,7 +296,6 @@ class HKDevice: await self.pairing.async_populate_accessories_state( force_update=True, attempts=attempts ) - self._async_start_polling() entry.async_on_unload(pairing.dispatcher_connect(self.process_new_events)) entry.async_on_unload( @@ -307,6 +308,12 @@ class HKDevice: await self.async_process_entity_map() + if transport != Transport.BLE: + # Do a single poll to make sure the chars are + # up to date so we don't restore old data. + await self.async_update() + self._async_start_polling() + # If everything is up to date, we can create the entities # since we know the data is not stale. await self.async_add_new_entities() @@ -711,9 +718,11 @@ class HKDevice: """Stop interacting with device and prepare for removal from hass.""" await self.pairing.shutdown() - await self.hass.config_entries.async_unload_platforms( - self.config_entry, self.platforms - ) + # Skip platform unloading during shutdown to preserve entity states + if not self.hass.is_stopping: + await self.hass.config_entries.async_unload_platforms( + self.config_entry, self.platforms + ) def process_config_changed(self, config_num: int) -> None: """Handle a config change notification from the pairing.""" diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index f26b11707c2..ec6f3099679 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool: identifiers={(DOMAIN, api.config.bridge_id)}, manufacturer="Signify", name=api.config.name, - model=api.config.model_id, + model_id=api.config.model_id, sw_version=api.config.software_version, ) # create persistent notification if we found a bridge version with security vulnerability @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool: }, manufacturer=api.config.bridge_device.product_data.manufacturer_name, name=api.config.name, - model=api.config.model_id, + model_id=api.config.model_id, sw_version=api.config.software_version, ) diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 7bb3d28e962..8979befcf73 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_MODEL_ID, ATTR_NAME, ATTR_SUGGESTED_AREA, ATTR_SW_VERSION, @@ -55,12 +56,12 @@ async def async_setup_devices(bridge: HueBridge): else None, ) # Register a Hue device resource as device in HA device registry. - model = f"{hue_resource.product_data.product_name} ({hue_resource.product_data.model_id})" params = { ATTR_IDENTIFIERS: {(DOMAIN, hue_resource.id)}, ATTR_SW_VERSION: hue_resource.product_data.software_version, ATTR_NAME: hue_resource.metadata.name, - ATTR_MODEL: model, + ATTR_MODEL: hue_resource.product_data.product_name, + ATTR_MODEL_ID: hue_resource.product_data.model_id, ATTR_MANUFACTURER: hue_resource.product_data.manufacturer_name, } if room := dev_controller.get_room(hue_resource.id): diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index 1945647a706..02adbc4adb6 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -21,6 +21,7 @@ PLATFORMS: list[Platform] = [ Platform.BUTTON, Platform.CALENDAR, Platform.DEVICE_TRACKER, + Platform.EVENT, Platform.LAWN_MOWER, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/husqvarna_automower/const.py b/homeassistant/components/husqvarna_automower/const.py index d91fea29698..f50c03e1b53 100644 --- a/homeassistant/components/husqvarna_automower/const.py +++ b/homeassistant/components/husqvarna_automower/const.py @@ -17,3 +17,128 @@ ERROR_STATES = [ MowerStates.WAIT_POWER_UP, MowerStates.WAIT_UPDATING, ] + +ERROR_KEYS = [ + "alarm_mower_in_motion", + "alarm_mower_lifted", + "alarm_mower_stopped", + "alarm_mower_switched_off", + "alarm_mower_tilted", + "alarm_outside_geofence", + "angular_sensor_problem", + "battery_problem", + "battery_restriction_due_to_ambient_temperature", + "can_error", + "charging_current_too_high", + "charging_station_blocked", + "charging_system_problem", + "collision_sensor_defect", + "collision_sensor_error", + "collision_sensor_problem_front", + "collision_sensor_problem_rear", + "com_board_not_available", + "communication_circuit_board_sw_must_be_updated", + "complex_working_area", + "connection_changed", + "connection_not_changed", + "connectivity_problem", + "connectivity_settings_restored", + "cutting_drive_motor_1_defect", + "cutting_drive_motor_2_defect", + "cutting_drive_motor_3_defect", + "cutting_height_blocked", + "cutting_height_problem", + "cutting_height_problem_curr", + "cutting_height_problem_dir", + "cutting_height_problem_drive", + "cutting_motor_problem", + "cutting_stopped_slope_too_steep", + "cutting_system_blocked", + "cutting_system_imbalance_warning", + "cutting_system_major_imbalance", + "destination_not_reachable", + "difficult_finding_home", + "docking_sensor_defect", + "electronic_problem", + "empty_battery", + "folding_cutting_deck_sensor_defect", + "folding_sensor_activated", + "geofence_problem", + "gps_navigation_problem", + "guide_1_not_found", + "guide_2_not_found", + "guide_3_not_found", + "guide_calibration_accomplished", + "guide_calibration_failed", + "high_charging_power_loss", + "high_internal_power_loss", + "high_internal_temperature", + "internal_voltage_error", + "invalid_battery_combination_invalid_combination_of_different_battery_types", + "invalid_sub_device_combination", + "invalid_system_configuration", + "left_brush_motor_overloaded", + "lift_sensor_defect", + "lifted", + "limited_cutting_height_range", + "loop_sensor_defect", + "loop_sensor_problem_front", + "loop_sensor_problem_left", + "loop_sensor_problem_rear", + "loop_sensor_problem_right", + "low_battery", + "memory_circuit_problem", + "mower_lifted", + "mower_tilted", + "no_accurate_position_from_satellites", + "no_confirmed_position", + "no_drive", + "no_loop_signal", + "no_power_in_charging_station", + "no_response_from_charger", + "outside_working_area", + "poor_signal_quality", + "reference_station_communication_problem", + "right_brush_motor_overloaded", + "safety_function_faulty", + "settings_restored", + "sim_card_locked", + "sim_card_not_found", + "sim_card_requires_pin", + "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern", + "slope_too_steep", + "sms_could_not_be_sent", + "stop_button_problem", + "stuck_in_charging_station", + "switch_cord_problem", + "temporary_battery_problem", + "tilt_sensor_problem", + "too_high_discharge_current", + "too_high_internal_current", + "trapped", + "ultrasonic_problem", + "ultrasonic_sensor_1_defect", + "ultrasonic_sensor_2_defect", + "ultrasonic_sensor_3_defect", + "ultrasonic_sensor_4_defect", + "unexpected_cutting_height_adj", + "unexpected_error", + "upside_down", + "weak_gps_signal", + "wheel_drive_problem_left", + "wheel_drive_problem_rear_left", + "wheel_drive_problem_rear_right", + "wheel_drive_problem_right", + "wheel_motor_blocked_left", + "wheel_motor_blocked_rear_left", + "wheel_motor_blocked_rear_right", + "wheel_motor_blocked_right", + "wheel_motor_overloaded_left", + "wheel_motor_overloaded_rear_left", + "wheel_motor_overloaded_rear_right", + "wheel_motor_overloaded_right", + "work_area_not_valid", + "wrong_loop_signal", + "wrong_pin_code", + "zone_generator_problem", +] diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index a037df474cc..dc35c47ff4a 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -12,6 +12,7 @@ from aioautomower.exceptions import ( ApiError, AuthError, HusqvarnaTimeoutError, + HusqvarnaWSClientError, HusqvarnaWSServerHandshakeError, ) from aioautomower.model import MowerDictionary, MowerStates @@ -172,7 +173,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]): # Reset reconnect time after successful connection self.reconnect_time = DEFAULT_RECONNECT_TIME await automower_client.start_listening() - except HusqvarnaWSServerHandshakeError as err: + except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err: _LOGGER.debug( "Failed to connect to websocket. Trying to reconnect: %s", err, diff --git a/homeassistant/components/husqvarna_automower/event.py b/homeassistant/components/husqvarna_automower/event.py new file mode 100644 index 00000000000..8e2e48b940d --- /dev/null +++ b/homeassistant/components/husqvarna_automower/event.py @@ -0,0 +1,108 @@ +"""Creates the event entities for supported mowers.""" + +from collections.abc import Callable + +from aioautomower.model import SingleMessageData + +from homeassistant.components.event import ( + DOMAIN as EVENT_DOMAIN, + EventEntity, + EventEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import AutomowerConfigEntry +from .const import ERROR_KEYS +from .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerBaseEntity + +PARALLEL_UPDATES = 1 + +ATTR_SEVERITY = "severity" +ATTR_LATITUDE = "latitude" +ATTR_LONGITUDE = "longitude" +ATTR_DATE_TIME = "date_time" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: AutomowerConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Automower message event entities. + + Entities are created dynamically based on messages received from the API, + but only for mowers that support message events. + """ + coordinator = config_entry.runtime_data + entity_registry = er.async_get(hass) + + restored_mowers = { + entry.unique_id.removesuffix("_message") + for entry in er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + if entry.domain == EVENT_DOMAIN + } + + async_add_entities( + AutomowerMessageEventEntity(mower_id, coordinator) + for mower_id in restored_mowers + if mower_id in coordinator.data + ) + + @callback + def _handle_message(msg: SingleMessageData) -> None: + if msg.id in restored_mowers: + return + + restored_mowers.add(msg.id) + async_add_entities([AutomowerMessageEventEntity(msg.id, coordinator)]) + + coordinator.api.register_single_message_callback(_handle_message) + + +class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity): + """EventEntity for Automower message events.""" + + entity_description: EventEntityDescription + _message_cb: Callable[[SingleMessageData], None] + _attr_translation_key = "message" + _attr_event_types = ERROR_KEYS + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + ) -> None: + """Initialize Automower message event entity.""" + super().__init__(mower_id, coordinator) + self._attr_unique_id = f"{mower_id}_message" + + @callback + def _handle(self, msg: SingleMessageData) -> None: + """Handle a message event from the API and trigger the event entity if it matches the entity's mower ID.""" + if msg.id != self.mower_id: + return + message = msg.attributes.message + self._trigger_event( + message.code, + { + ATTR_SEVERITY: message.severity, + ATTR_LATITUDE: message.latitude, + ATTR_LONGITUDE: message.longitude, + ATTR_DATE_TIME: message.time, + }, + ) + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callback when entity is added to hass.""" + await super().async_added_to_hass() + self.coordinator.api.register_single_message_callback(self._handle) + + async def async_will_remove_from_hass(self) -> None: + """Unregister WebSocket callback when entity is removed.""" + self.coordinator.api.unregister_single_message_callback(self._handle) diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json index 5ff5940bdf4..ba9bc82f156 100644 --- a/homeassistant/components/husqvarna_automower/icons.json +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -13,6 +13,11 @@ "default": "mdi:saw-blade" } }, + "event": { + "message": { + "default": "mdi:alert-circle-check-outline" + } + }, "number": { "cutting_height": { "default": "mdi:grass" diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index c5af18c6387..50be89e9d42 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from . import AutomowerConfigEntry -from .const import ERROR_STATES +from .const import ERROR_KEYS, ERROR_STATES from .coordinator import AutomowerDataUpdateCoordinator from .entity import ( AutomowerBaseEntity, @@ -42,132 +42,6 @@ PARALLEL_UPDATES = 0 ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment" -ERROR_KEYS = [ - "alarm_mower_in_motion", - "alarm_mower_lifted", - "alarm_mower_stopped", - "alarm_mower_switched_off", - "alarm_mower_tilted", - "alarm_outside_geofence", - "angular_sensor_problem", - "battery_problem", - "battery_restriction_due_to_ambient_temperature", - "can_error", - "charging_current_too_high", - "charging_station_blocked", - "charging_system_problem", - "collision_sensor_defect", - "collision_sensor_error", - "collision_sensor_problem_front", - "collision_sensor_problem_rear", - "com_board_not_available", - "communication_circuit_board_sw_must_be_updated", - "complex_working_area", - "connection_changed", - "connection_not_changed", - "connectivity_problem", - "connectivity_settings_restored", - "cutting_drive_motor_1_defect", - "cutting_drive_motor_2_defect", - "cutting_drive_motor_3_defect", - "cutting_height_blocked", - "cutting_height_problem", - "cutting_height_problem_curr", - "cutting_height_problem_dir", - "cutting_height_problem_drive", - "cutting_motor_problem", - "cutting_stopped_slope_too_steep", - "cutting_system_blocked", - "cutting_system_imbalance_warning", - "cutting_system_major_imbalance", - "destination_not_reachable", - "difficult_finding_home", - "docking_sensor_defect", - "electronic_problem", - "empty_battery", - "folding_cutting_deck_sensor_defect", - "folding_sensor_activated", - "geofence_problem", - "gps_navigation_problem", - "guide_1_not_found", - "guide_2_not_found", - "guide_3_not_found", - "guide_calibration_accomplished", - "guide_calibration_failed", - "high_charging_power_loss", - "high_internal_power_loss", - "high_internal_temperature", - "internal_voltage_error", - "invalid_battery_combination_invalid_combination_of_different_battery_types", - "invalid_sub_device_combination", - "invalid_system_configuration", - "left_brush_motor_overloaded", - "lift_sensor_defect", - "lifted", - "limited_cutting_height_range", - "loop_sensor_defect", - "loop_sensor_problem_front", - "loop_sensor_problem_left", - "loop_sensor_problem_rear", - "loop_sensor_problem_right", - "low_battery", - "memory_circuit_problem", - "mower_lifted", - "mower_tilted", - "no_accurate_position_from_satellites", - "no_confirmed_position", - "no_drive", - "no_loop_signal", - "no_power_in_charging_station", - "no_response_from_charger", - "outside_working_area", - "poor_signal_quality", - "reference_station_communication_problem", - "right_brush_motor_overloaded", - "safety_function_faulty", - "settings_restored", - "sim_card_locked", - "sim_card_not_found", - "sim_card_requires_pin", - "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern", - "slope_too_steep", - "sms_could_not_be_sent", - "stop_button_problem", - "stuck_in_charging_station", - "switch_cord_problem", - "temporary_battery_problem", - "tilt_sensor_problem", - "too_high_discharge_current", - "too_high_internal_current", - "trapped", - "ultrasonic_problem", - "ultrasonic_sensor_1_defect", - "ultrasonic_sensor_2_defect", - "ultrasonic_sensor_3_defect", - "ultrasonic_sensor_4_defect", - "unexpected_cutting_height_adj", - "unexpected_error", - "upside_down", - "weak_gps_signal", - "wheel_drive_problem_left", - "wheel_drive_problem_rear_left", - "wheel_drive_problem_rear_right", - "wheel_drive_problem_right", - "wheel_motor_blocked_left", - "wheel_motor_blocked_rear_left", - "wheel_motor_blocked_rear_right", - "wheel_motor_blocked_right", - "wheel_motor_overloaded_left", - "wheel_motor_overloaded_rear_left", - "wheel_motor_overloaded_rear_right", - "wheel_motor_overloaded_right", - "work_area_not_valid", - "wrong_loop_signal", - "wrong_pin_code", - "zone_generator_problem", -] - - ERROR_KEY_LIST = sorted( set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"} ) diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index bd8a9346552..c10e56ec7c8 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -58,6 +58,158 @@ "name": "Reset cutting blade usage time" } }, + "event": { + "message": { + "name": "Message", + "state_attributes": { + "event_type": { + "state": { + "alarm_mower_in_motion": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_in_motion%]", + "alarm_mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_lifted%]", + "alarm_mower_stopped": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_stopped%]", + "alarm_mower_switched_off": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_switched_off%]", + "alarm_mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_tilted%]", + "alarm_outside_geofence": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_outside_geofence%]", + "angular_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::angular_sensor_problem%]", + "battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_problem%]", + "battery_restriction_due_to_ambient_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_restriction_due_to_ambient_temperature%]", + "can_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::can_error%]", + "charging_current_too_high": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_current_too_high%]", + "charging_station_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_station_blocked%]", + "charging_system_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_system_problem%]", + "collision_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_defect%]", + "collision_sensor_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_error%]", + "collision_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_front%]", + "collision_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_rear%]", + "com_board_not_available": "[%key:component::husqvarna_automower::entity::sensor::error::state::com_board_not_available%]", + "communication_circuit_board_sw_must_be_updated": "[%key:component::husqvarna_automower::entity::sensor::error::state::communication_circuit_board_sw_must_be_updated%]", + "complex_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::complex_working_area%]", + "connection_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_changed%]", + "connection_not_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_not_changed%]", + "connectivity_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_problem%]", + "connectivity_settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_settings_restored%]", + "cutting_drive_motor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_1_defect%]", + "cutting_drive_motor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_2_defect%]", + "cutting_drive_motor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_3_defect%]", + "cutting_height_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_blocked%]", + "cutting_height_problem_curr": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_curr%]", + "cutting_height_problem_dir": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_dir%]", + "cutting_height_problem_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_drive%]", + "cutting_height_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem%]", + "cutting_motor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_motor_problem%]", + "cutting_stopped_slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_stopped_slope_too_steep%]", + "cutting_system_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_blocked%]", + "cutting_system_imbalance_warning": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_imbalance_warning%]", + "cutting_system_major_imbalance": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_major_imbalance%]", + "destination_not_reachable": "[%key:component::husqvarna_automower::entity::sensor::error::state::destination_not_reachable%]", + "difficult_finding_home": "[%key:component::husqvarna_automower::entity::sensor::error::state::difficult_finding_home%]", + "docking_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::docking_sensor_defect%]", + "electronic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::electronic_problem%]", + "empty_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::empty_battery%]", + "error_at_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::error_at_power_up%]", + "error": "[%key:common::state::error%]", + "fatal_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::fatal_error%]", + "folding_cutting_deck_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_cutting_deck_sensor_defect%]", + "folding_sensor_activated": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_sensor_activated%]", + "geofence_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::geofence_problem%]", + "gps_navigation_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::gps_navigation_problem%]", + "guide_1_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_1_not_found%]", + "guide_2_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_2_not_found%]", + "guide_3_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_3_not_found%]", + "guide_calibration_accomplished": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_accomplished%]", + "guide_calibration_failed": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_failed%]", + "high_charging_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_charging_power_loss%]", + "high_internal_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_power_loss%]", + "high_internal_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_temperature%]", + "internal_voltage_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::internal_voltage_error%]", + "invalid_battery_combination_invalid_combination_of_different_battery_types": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_battery_combination_invalid_combination_of_different_battery_types%]", + "invalid_sub_device_combination": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_sub_device_combination%]", + "invalid_system_configuration": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_system_configuration%]", + "left_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::left_brush_motor_overloaded%]", + "lift_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::lift_sensor_defect%]", + "lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::lifted%]", + "limited_cutting_height_range": "[%key:component::husqvarna_automower::entity::sensor::error::state::limited_cutting_height_range%]", + "loop_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_defect%]", + "loop_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_front%]", + "loop_sensor_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_left%]", + "loop_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_rear%]", + "loop_sensor_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_right%]", + "low_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::low_battery%]", + "memory_circuit_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::memory_circuit_problem%]", + "mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_lifted%]", + "mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_tilted%]", + "no_accurate_position_from_satellites": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_accurate_position_from_satellites%]", + "no_confirmed_position": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_confirmed_position%]", + "no_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_drive%]", + "no_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_error%]", + "no_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_loop_signal%]", + "no_power_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_power_in_charging_station%]", + "no_response_from_charger": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_response_from_charger%]", + "off": "[%key:common::state::off%]", + "outside_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::outside_working_area%]", + "poor_signal_quality": "[%key:component::husqvarna_automower::entity::sensor::error::state::poor_signal_quality%]", + "reference_station_communication_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::reference_station_communication_problem%]", + "right_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::right_brush_motor_overloaded%]", + "safety_function_faulty": "[%key:component::husqvarna_automower::entity::sensor::error::state::safety_function_faulty%]", + "settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::settings_restored%]", + "sim_card_locked": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_locked%]", + "sim_card_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_not_found%]", + "sim_card_requires_pin": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_requires_pin%]", + "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern": "[%key:component::husqvarna_automower::entity::sensor::error::state::slipped_mower_has_slipped_situation_not_solved_with_moving_pattern%]", + "slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::slope_too_steep%]", + "sms_could_not_be_sent": "[%key:component::husqvarna_automower::entity::sensor::error::state::sms_could_not_be_sent%]", + "stop_button_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::stop_button_problem%]", + "stopped": "[%key:common::state::stopped%]", + "stuck_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::stuck_in_charging_station%]", + "switch_cord_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::switch_cord_problem%]", + "temporary_battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::temporary_battery_problem%]", + "tilt_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::tilt_sensor_problem%]", + "too_high_discharge_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_discharge_current%]", + "too_high_internal_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_internal_current%]", + "trapped": "[%key:component::husqvarna_automower::entity::sensor::error::state::trapped%]", + "ultrasonic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_problem%]", + "ultrasonic_sensor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_1_defect%]", + "ultrasonic_sensor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_2_defect%]", + "ultrasonic_sensor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_3_defect%]", + "ultrasonic_sensor_4_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_4_defect%]", + "unexpected_cutting_height_adj": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_cutting_height_adj%]", + "unexpected_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_error%]", + "upside_down": "[%key:component::husqvarna_automower::entity::sensor::error::state::upside_down%]", + "wait_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_power_up%]", + "wait_updating": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_updating%]", + "weak_gps_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::weak_gps_signal%]", + "wheel_drive_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_left%]", + "wheel_drive_problem_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_left%]", + "wheel_drive_problem_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_right%]", + "wheel_drive_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_right%]", + "wheel_motor_blocked_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_left%]", + "wheel_motor_blocked_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_left%]", + "wheel_motor_blocked_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_right%]", + "wheel_motor_blocked_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_right%]", + "wheel_motor_overloaded_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_left%]", + "wheel_motor_overloaded_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_left%]", + "wheel_motor_overloaded_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_right%]", + "wheel_motor_overloaded_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_right%]", + "work_area_not_valid": "[%key:component::husqvarna_automower::entity::sensor::error::state::work_area_not_valid%]", + "wrong_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_loop_signal%]", + "wrong_pin_code": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_pin_code%]", + "zone_generator_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::zone_generator_problem%]" + } + }, + "severity": { + "state": { + "fatal": "Fatal", + "error": "[%key:common::state::error%]", + "warning": "Warning", + "info": "Info", + "debug": "Debug", + "sw": "Software", + "unknown": "Unknown" + } + } + } + } + }, "number": { "cutting_height": { "name": "Cutting height" diff --git a/homeassistant/components/husqvarna_automower_ble/__init__.py b/homeassistant/components/husqvarna_automower_ble/__init__.py index fd4521549a2..4537dec0e28 100644 --- a/homeassistant/components/husqvarna_automower_ble/__init__.py +++ b/homeassistant/components/husqvarna_automower_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from automower_ble.mower import Mower +from automower_ble.protocol import ResponseResult from bleak import BleakError from bleak_retry_connector import close_stale_connections_by_address, get_device @@ -37,12 +38,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: HusqvarnaConfigEntry) -> device = bluetooth.async_ble_device_from_address( hass, address, connectable=True ) or await get_device(address) - if not await mower.connect(device): - raise ConfigEntryNotReady + response_result = await mower.connect(device) + if response_result != ResponseResult.OK: + raise ConfigEntryNotReady( + f"Unable to connect to device {address}, mower returned {response_result}" + ) except (TimeoutError, BleakError) as exception: raise ConfigEntryNotReady( f"Unable to connect to device {address} due to {exception}" ) from exception + LOGGER.debug("connected and paired") model = await mower.get_model() diff --git a/homeassistant/components/husqvarna_automower_ble/coordinator.py b/homeassistant/components/husqvarna_automower_ble/coordinator.py index ef9ccfa5a47..cd5b4e06005 100644 --- a/homeassistant/components/husqvarna_automower_ble/coordinator.py +++ b/homeassistant/components/husqvarna_automower_ble/coordinator.py @@ -6,6 +6,7 @@ from datetime import timedelta from typing import TYPE_CHECKING from automower_ble.mower import Mower +from automower_ble.protocol import ResponseResult from bleak import BleakError from bleak_retry_connector import close_stale_connections_by_address @@ -62,7 +63,7 @@ class HusqvarnaCoordinator(DataUpdateCoordinator[dict[str, str | int]]): ) try: - if not await self.mower.connect(device): + if await self.mower.connect(device) is not ResponseResult.OK: raise UpdateFailed("Failed to connect") except BleakError as err: raise UpdateFailed("Failed to connect") from err diff --git a/homeassistant/components/husqvarna_automower_ble/entity.py b/homeassistant/components/husqvarna_automower_ble/entity.py index cb62f36027a..32e5873ab0e 100644 --- a/homeassistant/components/husqvarna_automower_ble/entity.py +++ b/homeassistant/components/husqvarna_automower_ble/entity.py @@ -2,7 +2,11 @@ from __future__ import annotations -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + DeviceInfo, + format_mac, +) from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,6 +27,8 @@ class HusqvarnaAutomowerBleEntity(CoordinatorEntity[HusqvarnaCoordinator]): identifiers={(DOMAIN, f"{coordinator.address}_{coordinator.channel_id}")}, manufacturer=MANUFACTURER, model_id=coordinator.model, + suggested_area="Garden", + connections={(CONNECTION_BLUETOOTH, format_mac(coordinator.address))}, ) @property diff --git a/homeassistant/components/husqvarna_automower_ble/lawn_mower.py b/homeassistant/components/husqvarna_automower_ble/lawn_mower.py index 4b4a16ba1db..78d39ddd96a 100644 --- a/homeassistant/components/husqvarna_automower_ble/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower_ble/lawn_mower.py @@ -2,7 +2,7 @@ from __future__ import annotations -from automower_ble.protocol import MowerActivity, MowerState +from automower_ble.protocol import MowerActivity, MowerState, ResponseResult from homeassistant.components import bluetooth from homeassistant.components.lawn_mower import ( @@ -107,7 +107,7 @@ class AutomowerLawnMower(HusqvarnaAutomowerBleEntity, LawnMowerEntity): device = bluetooth.async_ble_device_from_address( self.coordinator.hass, self.coordinator.address, connectable=True ) - if not await self.coordinator.mower.connect(device): + if await self.coordinator.mower.connect(device) is not ResponseResult.OK: return await self.coordinator.mower.mower_resume() @@ -126,7 +126,7 @@ class AutomowerLawnMower(HusqvarnaAutomowerBleEntity, LawnMowerEntity): device = bluetooth.async_ble_device_from_address( self.coordinator.hass, self.coordinator.address, connectable=True ) - if not await self.coordinator.mower.connect(device): + if await self.coordinator.mower.connect(device) is not ResponseResult.OK: return await self.coordinator.mower.mower_park() @@ -143,7 +143,7 @@ class AutomowerLawnMower(HusqvarnaAutomowerBleEntity, LawnMowerEntity): device = bluetooth.async_ble_device_from_address( self.coordinator.hass, self.coordinator.address, connectable=True ) - if not await self.coordinator.mower.connect(device): + if await self.coordinator.mower.connect(device) is not ResponseResult.OK: return await self.coordinator.mower.mower_pause() diff --git a/homeassistant/components/huum/const.py b/homeassistant/components/huum/const.py index 13663d31cd0..177c035f041 100644 --- a/homeassistant/components/huum/const.py +++ b/homeassistant/components/huum/const.py @@ -4,7 +4,7 @@ from homeassistant.const import Platform DOMAIN = "huum" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.NUMBER] CONFIG_STEAMER = 1 CONFIG_LIGHT = 2 diff --git a/homeassistant/components/huum/icons.json b/homeassistant/components/huum/icons.json new file mode 100644 index 00000000000..4281cdbde2a --- /dev/null +++ b/homeassistant/components/huum/icons.json @@ -0,0 +1,13 @@ +{ + "entity": { + "number": { + "humidity": { + "default": "mdi:water", + "range": { + "0": "mdi:water-off", + "1": "mdi:water" + } + } + } + } +} diff --git a/homeassistant/components/huum/manifest.json b/homeassistant/components/huum/manifest.json index 38001c58b35..79bfd9795cb 100644 --- a/homeassistant/components/huum/manifest.json +++ b/homeassistant/components/huum/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huum", "iot_class": "cloud_polling", - "requirements": ["huum==0.8.0"] + "requirements": ["huum==0.8.1"] } diff --git a/homeassistant/components/huum/number.py b/homeassistant/components/huum/number.py new file mode 100644 index 00000000000..daaf348c029 --- /dev/null +++ b/homeassistant/components/huum/number.py @@ -0,0 +1,64 @@ +"""Control for steamer.""" + +from __future__ import annotations + +import logging + +from huum.const import SaunaStatus + +from homeassistant.components.number import NumberEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import CONFIG_STEAMER, CONFIG_STEAMER_AND_LIGHT +from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator +from .entity import HuumBaseEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: HuumConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up steamer if applicable.""" + coordinator = config_entry.runtime_data + + # Light is configured for this sauna. + if coordinator.data.config in [CONFIG_STEAMER, CONFIG_STEAMER_AND_LIGHT]: + async_add_entities([HuumSteamer(coordinator)]) + + +class HuumSteamer(HuumBaseEntity, NumberEntity): + """Representation of a steamer.""" + + _attr_translation_key = "humidity" + _attr_native_max_value = 10 + _attr_native_min_value = 0 + _attr_native_step = 1 + + def __init__(self, coordinator: HuumDataUpdateCoordinator) -> None: + """Initialize the steamer.""" + super().__init__(coordinator) + + self._attr_unique_id = coordinator.config_entry.entry_id + + @property + def native_value(self) -> float: + """Return the current value.""" + return self.coordinator.data.humidity + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + target_temperature = self.coordinator.data.target_temperature + if ( + not target_temperature + or self.coordinator.data.status != SaunaStatus.ONLINE_HEATING + ): + return + + await self.coordinator.huum.turn_on( + temperature=target_temperature, humidity=int(value) + ) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/huum/strings.json b/homeassistant/components/huum/strings.json index 55ccf0fdd81..13c2e5c85f6 100644 --- a/homeassistant/components/huum/strings.json +++ b/homeassistant/components/huum/strings.json @@ -24,6 +24,11 @@ "light": { "name": "[%key:component::light::title%]" } + }, + "number": { + "humidity": { + "name": "[%key:component::sensor::entity_component::humidity::name%]" + } } } } diff --git a/homeassistant/components/hydrawise/strings.json b/homeassistant/components/hydrawise/strings.json index 47543aa2f8f..29b6d741a5e 100644 --- a/homeassistant/components/hydrawise/strings.json +++ b/homeassistant/components/hydrawise/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Hydrawise Login", + "title": "Hydrawise login", "description": "Please provide the username and password for your Hydrawise cloud account:", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -10,7 +10,7 @@ "api_key": "[%key:common::config_flow::data::api_key%]" }, "data_description": { - "api_key": "You can generate an API Key in the 'Account Details' section of the Hydrawise app" + "api_key": "You can generate an API key in the 'Account Details' section of the Hydrawise app" } }, "reauth_confirm": { diff --git a/homeassistant/components/icloud/services.py b/homeassistant/components/icloud/services.py index dbb843e8216..44a2e5d52f7 100644 --- a/homeassistant/components/icloud/services.py +++ b/homeassistant/components/icloud/services.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify -from .account import IcloudAccount +from .account import IcloudAccount, IcloudConfigEntry from .const import ( ATTR_ACCOUNT, ATTR_DEVICE_NAME, @@ -92,8 +92,10 @@ def lost_device(service: ServiceCall) -> None: def update_account(service: ServiceCall) -> None: """Call the update function of an iCloud account.""" if (account := service.data.get(ATTR_ACCOUNT)) is None: - for account in service.hass.data[DOMAIN].values(): - account.keep_alive() + # Update all accounts when no specific account is provided + entry: IcloudConfigEntry + for entry in service.hass.config_entries.async_loaded_entries(DOMAIN): + entry.runtime_data.keep_alive() else: _get_account(service.hass, account).keep_alive() @@ -102,17 +104,12 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount: if account_identifier is None: return None - icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) - if icloud_account is None: - for account in hass.data[DOMAIN].values(): - if account.username == account_identifier: - icloud_account = account + entry: IcloudConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + if entry.runtime_data.username == account_identifier: + return entry.runtime_data - if icloud_account is None: - raise ValueError( - f"No iCloud account with username or name {account_identifier}" - ) - return icloud_account + raise ValueError(f"No iCloud account with username or name {account_identifier}") @callback diff --git a/homeassistant/components/imeon_inverter/const.py b/homeassistant/components/imeon_inverter/const.py index fd08955c038..9cde40e01d7 100644 --- a/homeassistant/components/imeon_inverter/const.py +++ b/homeassistant/components/imeon_inverter/const.py @@ -7,3 +7,26 @@ TIMEOUT = 30 PLATFORMS = [ Platform.SENSOR, ] +ATTR_BATTERY_STATUS = ["charging", "discharging", "charged"] +ATTR_INVERTER_STATE = [ + "unsynchronized", + "grid_consumption", + "grid_injection", + "grid_synchronised_but_not_used", +] +ATTR_TIMELINE_STATUS = [ + "com_lost", + "warning_grid", + "warning_pv", + "warning_bat", + "error_ond", + "error_soft", + "error_pv", + "error_grid", + "error_bat", + "good_1", + "info_soft", + "info_ond", + "info_bat", + "info_smartlo", +] diff --git a/homeassistant/components/imeon_inverter/coordinator.py b/homeassistant/components/imeon_inverter/coordinator.py index f1963a45579..3cb2b53d993 100644 --- a/homeassistant/components/imeon_inverter/coordinator.py +++ b/homeassistant/components/imeon_inverter/coordinator.py @@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import TIMEOUT HUBNAME = "imeon_inverter_hub" -INTERVAL = timedelta(seconds=60) +INTERVAL = 60 _LOGGER = logging.getLogger(__name__) type InverterConfigEntry = ConfigEntry[InverterCoordinator] @@ -44,7 +44,7 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]): hass, _LOGGER, name=HUBNAME, - update_interval=INTERVAL, + update_interval=timedelta(seconds=INTERVAL), config_entry=entry, ) @@ -75,15 +75,13 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]): data: dict[str, str | float | int] = {} async with timeout(TIMEOUT): - await self._api.login( - self.config_entry.data[CONF_USERNAME], - self.config_entry.data[CONF_PASSWORD], - ) - - # Fetch data using distant API try: + await self._api.login( + self.config_entry.data[CONF_USERNAME], + self.config_entry.data[CONF_PASSWORD], + ) await self._api.update() - except (ValueError, ClientError) as e: + except (ValueError, TimeoutError, ClientError) as e: raise UpdateFailed(e) from e # Store data diff --git a/homeassistant/components/imeon_inverter/icons.json b/homeassistant/components/imeon_inverter/icons.json index 6ede2416afa..a4a7edf21a6 100644 --- a/homeassistant/components/imeon_inverter/icons.json +++ b/homeassistant/components/imeon_inverter/icons.json @@ -7,8 +7,14 @@ "battery_soc": { "default": "mdi:battery-charging-100" }, + "battery_status": { + "default": "mdi:battery-alert" + }, "battery_stored": { - "default": "mdi:battery" + "default": "mdi:battery-arrow-up" + }, + "battery_consumed": { + "default": "mdi:battery-arrow-down" }, "grid_current_l1": { "default": "mdi:current-ac" @@ -50,7 +56,7 @@ "default": "mdi:power-socket" }, "meter_power": { - "default": "mdi:power-plug" + "default": "mdi:meter-electric" }, "output_current_l1": { "default": "mdi:current-ac" @@ -116,7 +122,7 @@ "default": "mdi:home-lightning-bolt" }, "monitoring_minute_grid_consumption": { - "default": "mdi:transmission-tower" + "default": "mdi:transmission-tower-import" }, "monitoring_minute_grid_injection": { "default": "mdi:transmission-tower-export" @@ -126,6 +132,43 @@ }, "monitoring_minute_solar_production": { "default": "mdi:solar-power" + }, + "timeline_type_msg": { + "default": "mdi:check-circle", + "state": { + "com_lost": "mdi:lan-disconnect", + "warning_grid": "mdi:alert-circle", + "warning_pv": "mdi:alert-circle", + "warning_bat": "mdi:alert-circle", + "error_ond": "mdi:close-octagon", + "error_soft": "mdi:close-octagon", + "error_pv": "mdi:close-octagon", + "error_grid": "mdi:close-octagon", + "error_bat": "mdi:close-octagon", + "good_1": "mdi:check-circle", + "info_soft": "mdi:information-slab-circle", + "info_ond": "mdi:information-slab-circle", + "info_bat": "mdi:information-slab-circle", + "info_smartlo": "mdi:information-slab-circle" + } + }, + "energy_pv": { + "default": "mdi:solar-power" + }, + "energy_grid_injected": { + "default": "mdi:transmission-tower-export" + }, + "energy_grid_consumed": { + "default": "mdi:transmission-tower-import" + }, + "energy_building_consumption": { + "default": "mdi:home-lightning-bolt-outline" + }, + "energy_battery_stored": { + "default": "mdi:battery-arrow-up-outline" + }, + "energy_battery_consumed": { + "default": "mdi:battery-arrow-down-outline" } } } diff --git a/homeassistant/components/imeon_inverter/sensor.py b/homeassistant/components/imeon_inverter/sensor.py index 32d40923fa1..21aa37a0523 100644 --- a/homeassistant/components/imeon_inverter/sensor.py +++ b/homeassistant/components/imeon_inverter/sensor.py @@ -23,6 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType +from .const import ATTR_BATTERY_STATUS, ATTR_INVERTER_STATE, ATTR_TIMELINE_STATUS from .coordinator import InverterCoordinator from .entity import InverterEntity @@ -47,11 +48,24 @@ SENSOR_DESCRIPTIONS = ( device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="battery_status", + translation_key="battery_status", + device_class=SensorDeviceClass.ENUM, + options=ATTR_BATTERY_STATUS, + ), SensorEntityDescription( key="battery_stored", translation_key="battery_stored", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY_STORAGE, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="battery_consumed", + translation_key="battery_consumed", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), # Grid @@ -148,6 +162,12 @@ SENSOR_DESCRIPTIONS = ( device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="manager_inverter_state", + translation_key="manager_inverter_state", + device_class=SensorDeviceClass.ENUM, + options=ATTR_INVERTER_STATE, + ), # Meter SensorEntityDescription( key="meter_power", @@ -238,16 +258,16 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key="pv_consumed", translation_key="pv_consumed", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pv_injected", translation_key="pv_injected", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="pv_power_1", @@ -290,14 +310,14 @@ SENSOR_DESCRIPTIONS = ( key="monitoring_self_consumption", translation_key="monitoring_self_consumption", native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, suggested_display_precision=2, ), SensorEntityDescription( key="monitoring_self_sufficiency", translation_key="monitoring_self_sufficiency", native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, suggested_display_precision=2, ), # Monitoring (instant minute data) @@ -341,6 +361,62 @@ SENSOR_DESCRIPTIONS = ( state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, ), + # Timeline + SensorEntityDescription( + key="timeline_type_msg", + translation_key="timeline_type_msg", + device_class=SensorDeviceClass.ENUM, + options=ATTR_TIMELINE_STATUS, + ), + # Daily energy counters + SensorEntityDescription( + key="energy_pv", + translation_key="energy_pv", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_grid_injected", + translation_key="energy_grid_injected", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_grid_consumed", + translation_key="energy_grid_consumed", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_building_consumption", + translation_key="energy_building_consumption", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_battery_stored", + translation_key="energy_battery_stored", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_battery_consumed", + translation_key="energy_battery_consumed", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), ) diff --git a/homeassistant/components/imeon_inverter/strings.json b/homeassistant/components/imeon_inverter/strings.json index 86855361b8f..66d0472b89a 100644 --- a/homeassistant/components/imeon_inverter/strings.json +++ b/homeassistant/components/imeon_inverter/strings.json @@ -35,9 +35,20 @@ "battery_soc": { "name": "Battery state of charge" }, + "battery_status": { + "name": "Battery status", + "state": { + "charged": "Charged", + "charging": "[%key:common::state::charging%]", + "discharging": "[%key:common::state::discharging%]" + } + }, "battery_stored": { "name": "Battery stored" }, + "battery_consumed": { + "name": "Battery consumed" + }, "grid_current_l1": { "name": "Grid current L1" }, @@ -77,6 +88,15 @@ "inverter_injection_power_limit": { "name": "Injection power limit" }, + "manager_inverter_state": { + "name": "Inverter state", + "state": { + "unsynchronized": "Unsynchronized", + "grid_consumption": "Grid consumption", + "grid_injection": "Grid injection", + "grid_synchronised_but_not_used": "Grid unsynchronized but used" + } + }, "meter_power": { "name": "Meter power" }, @@ -135,25 +155,63 @@ "name": "Component temperature" }, "monitoring_self_consumption": { - "name": "Monitoring self-consumption" + "name": "Self-consumption" }, "monitoring_self_sufficiency": { - "name": "Monitoring self-sufficiency" + "name": "Self-sufficiency" }, "monitoring_minute_building_consumption": { - "name": "Monitoring building consumption (minute)" + "name": "Building consumption" }, "monitoring_minute_grid_consumption": { - "name": "Monitoring grid consumption (minute)" + "name": "Grid consumption" }, "monitoring_minute_grid_injection": { - "name": "Monitoring grid injection (minute)" + "name": "Grid injection" }, "monitoring_minute_grid_power_flow": { - "name": "Monitoring grid power flow (minute)" + "name": "Grid power flow" }, "monitoring_minute_solar_production": { - "name": "Monitoring solar production (minute)" + "name": "Solar production" + }, + "timeline_type_msg": { + "name": "Timeline status", + "state": { + "com_lost": "Communication lost.", + "warning_grid": "Power grid warning detected.", + "warning_pv": "PV system warning detected.", + "warning_bat": "Battery warning detected.", + "error_ond": "Inverter error detected.", + "error_soft": "Software error detected.", + "error_pv": "PV system error detected.", + "error_grid": "Power grid error detected.", + "error_bat": "Battery error detected.", + "good_1": "System operating normally.", + "web_account": "Web account notification.", + "info_soft": "Software information available.", + "info_ond": "Inverter information available.", + "info_bat": "Battery information available.", + "info_smartlo": "Smart load information available." + } + }, + "energy_pv": { + "name": "Today PV energy" + }, + "energy_grid_injected": { + "name": "Today grid-injected energy" + }, + "energy_grid_consumed": { + "name": "Today grid-consumed energy" + }, + "energy_building_consumption": { + "name": "Today building consumption" + }, + "energy_battery_stored": { + "name": "Today battery-stored energy" + }, + "energy_battery_consumed": { + "name": "Today battery-consumed energy" } } } diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index e65ccf35fb5..b0779b35f14 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["imgw_pib==1.5.2"] + "requirements": ["imgw_pib==1.5.4"] } diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 8e01b6b6ae0..0f5a066600c 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -29,7 +29,8 @@ from .const import ( DEFAULT_LANGUAGE, DOMAIN, ) -from .entity import JewishCalendarConfigEntry, JewishCalendarData +from .coordinator import JewishCalendarData, JewishCalendarUpdateCoordinator +from .entity import JewishCalendarConfigEntry from .services import async_setup_services _LOGGER = logging.getLogger(__name__) @@ -69,7 +70,7 @@ async def async_setup_entry( ) ) - config_entry.runtime_data = JewishCalendarData( + data = JewishCalendarData( language, diaspora, location, @@ -77,8 +78,11 @@ async def async_setup_entry( havdalah_offset, ) - await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + coordinator = JewishCalendarUpdateCoordinator(hass, config_entry, data) + await coordinator.async_config_entry_first_refresh() + config_entry.runtime_data = coordinator + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -86,7 +90,13 @@ async def async_unload_entry( hass: HomeAssistant, config_entry: JewishCalendarConfigEntry ) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + if unload_ok := await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ): + coordinator = config_entry.runtime_data + if coordinator.event_unsub: + coordinator.event_unsub() + return unload_ok async def async_migrate_entry( diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index d5097df962f..205691bc183 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -72,8 +72,7 @@ class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - zmanim = self.make_zmanim(dt.date.today()) - return self.entity_description.is_on(zmanim)(dt_util.now()) + return self.entity_description.is_on(self.coordinator.zmanim)(dt_util.now()) def _update_times(self, zmanim: Zmanim) -> list[dt.datetime | None]: """Return a list of times to update the sensor.""" diff --git a/homeassistant/components/jewish_calendar/coordinator.py b/homeassistant/components/jewish_calendar/coordinator.py new file mode 100644 index 00000000000..21713313043 --- /dev/null +++ b/homeassistant/components/jewish_calendar/coordinator.py @@ -0,0 +1,116 @@ +"""Data update coordinator for Jewish calendar.""" + +from dataclasses import dataclass +import datetime as dt +import logging + +from hdate import HDateInfo, Location, Zmanim +from hdate.translator import Language, set_language + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers import event +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarUpdateCoordinator] + + +@dataclass +class JewishCalendarData: + """Jewish Calendar runtime dataclass.""" + + language: Language + diaspora: bool + location: Location + candle_lighting_offset: int + havdalah_offset: int + dateinfo: HDateInfo | None = None + zmanim: Zmanim | None = None + + +class JewishCalendarUpdateCoordinator(DataUpdateCoordinator[JewishCalendarData]): + """Data update coordinator class for Jewish calendar.""" + + config_entry: JewishCalendarConfigEntry + event_unsub: CALLBACK_TYPE | None = None + + def __init__( + self, + hass: HomeAssistant, + config_entry: JewishCalendarConfigEntry, + data: JewishCalendarData, + ) -> None: + """Initialize the coordinator.""" + super().__init__(hass, _LOGGER, name=DOMAIN, config_entry=config_entry) + self.data = data + self._unsub_update: CALLBACK_TYPE | None = None + set_language(data.language) + + async def _async_update_data(self) -> JewishCalendarData: + """Return HDate and Zmanim for today.""" + now = dt_util.now() + _LOGGER.debug("Now: %s Location: %r", now, self.data.location) + + today = now.date() + + self.data.dateinfo = HDateInfo(today, self.data.diaspora) + self.data.zmanim = self.make_zmanim(today) + self.async_schedule_future_update() + return self.data + + @callback + def async_schedule_future_update(self) -> None: + """Schedule the next update of the sensor for the upcoming midnight.""" + # Cancel any existing update + if self._unsub_update: + self._unsub_update() + self._unsub_update = None + + # Calculate the next midnight + next_midnight = dt_util.start_of_local_day() + dt.timedelta(days=1) + + _LOGGER.debug("Scheduling next update at %s", next_midnight) + + # Schedule update at next midnight + self._unsub_update = event.async_track_point_in_time( + self.hass, self._handle_midnight_update, next_midnight + ) + + @callback + def _handle_midnight_update(self, _now: dt.datetime) -> None: + """Handle midnight update callback.""" + self._unsub_update = None + self.async_set_updated_data(self.data) + + async def async_shutdown(self) -> None: + """Cancel any scheduled updates when the coordinator is shutting down.""" + await super().async_shutdown() + if self._unsub_update: + self._unsub_update() + self._unsub_update = None + + def make_zmanim(self, date: dt.date) -> Zmanim: + """Create a Zmanim object.""" + return Zmanim( + date=date, + location=self.data.location, + candle_lighting_offset=self.data.candle_lighting_offset, + havdalah_offset=self.data.havdalah_offset, + ) + + @property + def zmanim(self) -> Zmanim: + """Return the current Zmanim.""" + assert self.data.zmanim is not None, "Zmanim data not available" + return self.data.zmanim + + @property + def dateinfo(self) -> HDateInfo: + """Return the current HDateInfo.""" + assert self.data.dateinfo is not None, "HDateInfo data not available" + return self.data.dateinfo diff --git a/homeassistant/components/jewish_calendar/diagnostics.py b/homeassistant/components/jewish_calendar/diagnostics.py index 27415282b6d..f2db0786b12 100644 --- a/homeassistant/components/jewish_calendar/diagnostics.py +++ b/homeassistant/components/jewish_calendar/diagnostics.py @@ -24,5 +24,5 @@ async def async_get_config_entry_diagnostics( return { "entry_data": async_redact_data(entry.data, TO_REDACT), - "data": async_redact_data(asdict(entry.runtime_data), TO_REDACT), + "data": async_redact_data(asdict(entry.runtime_data.data), TO_REDACT), } diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py index d5e41129075..d3007212739 100644 --- a/homeassistant/components/jewish_calendar/entity.py +++ b/homeassistant/components/jewish_calendar/entity.py @@ -1,48 +1,22 @@ """Entity representing a Jewish Calendar sensor.""" from abc import abstractmethod -from dataclasses import dataclass import datetime as dt -import logging -from hdate import HDateInfo, Location, Zmanim -from hdate.translator import Language, set_language +from hdate import Zmanim -from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import event from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo -from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData] +from .coordinator import JewishCalendarConfigEntry, JewishCalendarUpdateCoordinator -@dataclass -class JewishCalendarDataResults: - """Jewish Calendar results dataclass.""" - - dateinfo: HDateInfo - zmanim: Zmanim - - -@dataclass -class JewishCalendarData: - """Jewish Calendar runtime dataclass.""" - - language: Language - diaspora: bool - location: Location - candle_lighting_offset: int - havdalah_offset: int - results: JewishCalendarDataResults | None = None - - -class JewishCalendarEntity(Entity): +class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]): """An HA implementation for Jewish Calendar entity.""" _attr_has_entity_name = True @@ -55,23 +29,13 @@ class JewishCalendarEntity(Entity): description: EntityDescription, ) -> None: """Initialize a Jewish Calendar entity.""" + super().__init__(config_entry.runtime_data) self.entity_description = description self._attr_unique_id = f"{config_entry.entry_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry.entry_id)}, ) - self.data = config_entry.runtime_data - set_language(self.data.language) - - def make_zmanim(self, date: dt.date) -> Zmanim: - """Create a Zmanim object.""" - return Zmanim( - date=date, - location=self.data.location, - candle_lighting_offset=self.data.candle_lighting_offset, - havdalah_offset=self.data.havdalah_offset, - ) async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" @@ -92,10 +56,9 @@ class JewishCalendarEntity(Entity): def _schedule_update(self) -> None: """Schedule the next update of the sensor.""" now = dt_util.now() - zmanim = self.make_zmanim(now.date()) update = dt_util.start_of_local_day() + dt.timedelta(days=1) - for update_time in self._update_times(zmanim): + for update_time in self._update_times(self.coordinator.zmanim): if update_time is not None and now < update_time < update: update = update_time @@ -110,17 +73,4 @@ class JewishCalendarEntity(Entity): """Update the sensor data.""" self._update_unsub = None self._schedule_update() - self.create_results(now) self.async_write_ha_state() - - def create_results(self, now: dt.datetime | None = None) -> None: - """Create the results for the sensor.""" - if now is None: - now = dt_util.now() - - _LOGGER.debug("Now: %s Location: %r", now, self.data.location) - - today = now.date() - zmanim = self.make_zmanim(today) - dateinfo = HDateInfo(today, diaspora=self.data.diaspora) - self.data.results = JewishCalendarDataResults(dateinfo, zmanim) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index d9ad89237f5..579c8e0f6a6 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -19,7 +19,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.util import dt as dt_util +import homeassistant.util.dt as dt_util from .entity import JewishCalendarConfigEntry, JewishCalendarEntity @@ -236,25 +236,18 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): return [] return [self.entity_description.next_update_fn(zmanim)] - def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo: + def get_dateinfo(self) -> HDateInfo: """Get the next date info.""" - if self.data.results is None: - self.create_results() - assert self.data.results is not None, "Results should be available" - - if now is None: - now = dt_util.now() - - today = now.date() - zmanim = self.make_zmanim(today) + now = dt_util.now() update = None - if self.entity_description.next_update_fn: - update = self.entity_description.next_update_fn(zmanim) - _LOGGER.debug("Today: %s, update: %s", today, update) + if self.entity_description.next_update_fn: + update = self.entity_description.next_update_fn(self.coordinator.zmanim) + + _LOGGER.debug("Today: %s, update: %s", now.date(), update) if update is not None and now >= update: - return self.data.results.dateinfo.next_day - return self.data.results.dateinfo + return self.coordinator.dateinfo.next_day + return self.coordinator.dateinfo class JewishCalendarSensor(JewishCalendarBaseSensor): @@ -271,7 +264,9 @@ class JewishCalendarSensor(JewishCalendarBaseSensor): super().__init__(config_entry, description) # Set the options for enumeration sensors if self.entity_description.options_fn is not None: - self._attr_options = self.entity_description.options_fn(self.data.diaspora) + self._attr_options = self.entity_description.options_fn( + self.coordinator.data.diaspora + ) @property def native_value(self) -> str | int | dt.datetime | None: @@ -295,9 +290,8 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor): @property def native_value(self) -> dt.datetime | None: """Return the state of the sensor.""" - if self.data.results is None: - self.create_results() - assert self.data.results is not None, "Results should be available" if self.entity_description.value_fn is None: - return self.data.results.zmanim.zmanim[self.entity_description.key].local - return self.entity_description.value_fn(self.get_dateinfo(), self.make_zmanim) + return self.coordinator.zmanim.zmanim[self.entity_description.key].local + return self.entity_description.value_fn( + self.get_dateinfo(), self.coordinator.make_zmanim + ) diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py index f85c3f33f93..1d0e6a4c1bc 100644 --- a/homeassistant/components/justnimbus/entity.py +++ b/homeassistant/components/justnimbus/entity.py @@ -28,6 +28,7 @@ class JustNimbusEntity( identifiers={(DOMAIN, device_id)}, name="JustNimbus Sensor", manufacturer="JustNimbus", + sw_version=coordinator.data.api_version, ) @property diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py index 056ace7011c..27a10738f48 100644 --- a/homeassistant/components/kitchen_sink/config_flow.py +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -146,7 +146,7 @@ class SubentryFlowHandler(ConfigSubentryFlow): """Reconfigure a sensor.""" if user_input is not None: title = user_input.pop("name") - return self.async_update_and_abort( + return self.async_update_reload_and_abort( self._get_entry(), self._get_reconfigure_subentry(), data=user_input, diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f3013de4556..508603ec66e 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,7 +13,7 @@ "requirements": [ "xknx==3.8.0", "xknxproject==3.8.2", - "knx-frontend==2025.8.6.52906" + "knx-frontend==2025.8.24.205840" ], "single_config_entry": true } diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index 6c41a7d29e7..fe0dbf31b6b 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -27,7 +27,6 @@ from ..const import ( ColorTempModes, CoverConf, ) -from ..validation import sync_state_validator from .const import ( CONF_COLOR, CONF_COLOR_TEMP_MAX, @@ -57,7 +56,14 @@ from .const import ( CONF_GA_WHITE_BRIGHTNESS, CONF_GA_WHITE_SWITCH, ) -from .knx_selector import GASelector, GroupSelect +from .knx_selector import ( + AllSerializeFirst, + GASelector, + GroupSelect, + GroupSelectOption, + KNXSectionFlat, + SyncStateSelector, +) BASE_ENTITY_SCHEMA = vol.All( { @@ -85,86 +91,86 @@ BASE_ENTITY_SCHEMA = vol.All( ) -BINARY_SENSOR_SCHEMA = vol.Schema( +BINARY_SENSOR_KNX_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, - vol.Required(DOMAIN): { - vol.Required(CONF_GA_SENSOR): GASelector(write=False, state_required=True), - vol.Required(CONF_RESPOND_TO_READ, default=False): bool, - vol.Required(CONF_SYNC_STATE, default=True): sync_state_validator, - vol.Optional(CONF_INVERT): selector.BooleanSelector(), - vol.Optional(CONF_IGNORE_INTERNAL_STATE): selector.BooleanSelector(), - vol.Optional(CONF_CONTEXT_TIMEOUT): selector.NumberSelector( - selector.NumberSelectorConfig( - min=0, max=10, step=0.1, unit_of_measurement="s" - ) - ), - vol.Optional(CONF_RESET_AFTER): selector.NumberSelector( - selector.NumberSelectorConfig( - min=0, max=600, step=0.1, unit_of_measurement="s" - ) - ), - }, - } + "section_binary_sensor": KNXSectionFlat(), + vol.Required(CONF_GA_SENSOR): GASelector( + write=False, state_required=True, valid_dpt="1" + ), + vol.Optional(CONF_INVERT): selector.BooleanSelector(), + "section_advanced_options": KNXSectionFlat(collapsible=True), + vol.Optional(CONF_IGNORE_INTERNAL_STATE): selector.BooleanSelector(), + vol.Optional(CONF_CONTEXT_TIMEOUT): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, max=10, step=0.1, unit_of_measurement="s" + ) + ), + vol.Optional(CONF_RESET_AFTER): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, max=600, step=0.1, unit_of_measurement="s" + ) + ), + vol.Required(CONF_SYNC_STATE, default=True): SyncStateSelector(), + }, ) -COVER_SCHEMA = vol.Schema( - { - vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, - vol.Required(DOMAIN): vol.All( - vol.Schema( - { - vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False), - vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(), - vol.Optional(CONF_GA_STOP): GASelector(state=False), - vol.Optional(CONF_GA_STEP): GASelector(state=False), - vol.Optional(CONF_GA_POSITION_SET): GASelector(state=False), - vol.Optional(CONF_GA_POSITION_STATE): GASelector(write=False), - vol.Optional(CoverConf.INVERT_POSITION): selector.BooleanSelector(), - vol.Optional(CONF_GA_ANGLE): GASelector(), - vol.Optional(CoverConf.INVERT_ANGLE): selector.BooleanSelector(), - vol.Optional( - CoverConf.TRAVELLING_TIME_DOWN, default=25 - ): selector.NumberSelector( - selector.NumberSelectorConfig( - min=0, max=1000, step=0.1, unit_of_measurement="s" - ) - ), - vol.Optional( - CoverConf.TRAVELLING_TIME_UP, default=25 - ): selector.NumberSelector( - selector.NumberSelectorConfig( - min=0, max=1000, step=0.1, unit_of_measurement="s" - ) - ), - vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, - }, - extra=vol.REMOVE_EXTRA, +COVER_KNX_SCHEMA = AllSerializeFirst( + vol.Schema( + { + "section_binary_control": KNXSectionFlat(), + vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False), + vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(), + "section_stop_control": KNXSectionFlat(), + vol.Optional(CONF_GA_STOP): GASelector(state=False), + vol.Optional(CONF_GA_STEP): GASelector(state=False), + "section_position_control": KNXSectionFlat(collapsible=True), + vol.Optional(CONF_GA_POSITION_SET): GASelector(state=False), + vol.Optional(CONF_GA_POSITION_STATE): GASelector(write=False), + vol.Optional(CoverConf.INVERT_POSITION): selector.BooleanSelector(), + "section_tilt_control": KNXSectionFlat(collapsible=True), + vol.Optional(CONF_GA_ANGLE): GASelector(), + vol.Optional(CoverConf.INVERT_ANGLE): selector.BooleanSelector(), + "section_travel_time": KNXSectionFlat(), + vol.Optional( + CoverConf.TRAVELLING_TIME_UP, default=25 + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, max=1000, step=0.1, unit_of_measurement="s" + ) ), - vol.Any( - vol.Schema( - { - vol.Required(CONF_GA_UP_DOWN): GASelector( - state=False, write_required=True - ) - }, - extra=vol.ALLOW_EXTRA, - ), - vol.Schema( - { - vol.Required(CONF_GA_POSITION_SET): GASelector( - state=False, write_required=True - ) - }, - extra=vol.ALLOW_EXTRA, - ), - msg=( - "At least one of 'Up/Down control' or" - " 'Position - Set position' is required." - ), + vol.Optional( + CoverConf.TRAVELLING_TIME_DOWN, default=25 + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, max=1000, step=0.1, unit_of_measurement="s" + ) ), + vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(), + }, + extra=vol.REMOVE_EXTRA, + ), + vol.Any( + vol.Schema( + { + vol.Required(CONF_GA_UP_DOWN): GASelector( + state=False, write_required=True + ) + }, + extra=vol.ALLOW_EXTRA, ), - } + vol.Schema( + { + vol.Required(CONF_GA_POSITION_SET): GASelector( + state=False, write_required=True + ) + }, + extra=vol.ALLOW_EXTRA, + ), + msg=( + "At least one of 'Open/Close control' or" + " 'Position - Set position' is required." + ), + ), ) @@ -177,81 +183,91 @@ class LightColorMode(StrEnum): XYY = "242.600" -@unique -class LightColorModeSchema(StrEnum): - """Enum for light color mode.""" - - DEFAULT = "default" - INDIVIDUAL = "individual" - HSV = "hsv" - - _hs_color_inclusion_msg = ( "'Hue', 'Saturation' and 'Brightness' addresses are required for HSV configuration" ) -LIGHT_KNX_SCHEMA = vol.All( +LIGHT_KNX_SCHEMA = AllSerializeFirst( vol.Schema( { - vol.Optional(CONF_GA_SWITCH): GASelector(write_required=True), - vol.Optional(CONF_GA_BRIGHTNESS): GASelector(write_required=True), + "section_switch": KNXSectionFlat(), + vol.Optional(CONF_GA_SWITCH): GASelector( + write_required=True, valid_dpt="1" + ), + "section_brightness": KNXSectionFlat(), + vol.Optional(CONF_GA_BRIGHTNESS): GASelector( + write_required=True, valid_dpt="5.001" + ), + "section_color_temp": KNXSectionFlat(collapsible=True), vol.Optional(CONF_GA_COLOR_TEMP): GASelector( write_required=True, dpt=ColorTempModes ), + vol.Required(CONF_COLOR_TEMP_MIN, default=2700): selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=10000, step=1, unit_of_measurement="K" + ) + ), + vol.Required(CONF_COLOR_TEMP_MAX, default=6000): selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=10000, step=1, unit_of_measurement="K" + ) + ), vol.Optional(CONF_COLOR): GroupSelect( - vol.Schema( - { + GroupSelectOption( + translation_key="single_address", + schema={ vol.Optional(CONF_GA_COLOR): GASelector( write_required=True, dpt=LightColorMode ) - } + }, ), - vol.Schema( - { - vol.Required(CONF_GA_RED_BRIGHTNESS): GASelector( - write_required=True - ), + GroupSelectOption( + translation_key="individual_addresses", + schema={ + "section_red": KNXSectionFlat(), vol.Optional(CONF_GA_RED_SWITCH): GASelector( - write_required=False + write_required=False, valid_dpt="1" + ), + vol.Required(CONF_GA_RED_BRIGHTNESS): GASelector( + write_required=True, valid_dpt="5.001" + ), + "section_green": KNXSectionFlat(), + vol.Optional(CONF_GA_GREEN_SWITCH): GASelector( + write_required=False, valid_dpt="1" ), vol.Required(CONF_GA_GREEN_BRIGHTNESS): GASelector( - write_required=True - ), - vol.Optional(CONF_GA_GREEN_SWITCH): GASelector( - write_required=False + write_required=True, valid_dpt="5.001" ), + "section_blue": KNXSectionFlat(), vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector( - write_required=True + write_required=True, valid_dpt="5.001" ), vol.Optional(CONF_GA_BLUE_SWITCH): GASelector( - write_required=False + write_required=False, valid_dpt="1" ), + "section_white": KNXSectionFlat(), vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector( - write_required=True + write_required=True, valid_dpt="5.001" ), vol.Optional(CONF_GA_WHITE_SWITCH): GASelector( - write_required=False + write_required=False, valid_dpt="1" ), - } + }, ), - vol.Schema( - { - vol.Required(CONF_GA_HUE): GASelector(write_required=True), + GroupSelectOption( + translation_key="hsv_addresses", + schema={ + vol.Required(CONF_GA_HUE): GASelector( + write_required=True, valid_dpt="5.001" + ), vol.Required(CONF_GA_SATURATION): GASelector( - write_required=True + write_required=True, valid_dpt="5.001" ), - } + }, ), - # msg="error in `color` config", - ), - vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, - vol.Optional(CONF_COLOR_TEMP_MIN, default=2700): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Optional(CONF_COLOR_TEMP_MAX, default=6000): vol.All( - vol.Coerce(int), vol.Range(min=1) ), + vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(), } ), vol.Any( @@ -291,26 +307,22 @@ LIGHT_KNX_SCHEMA = vol.All( ), ) - -LIGHT_SCHEMA = vol.Schema( +SWITCH_KNX_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, - vol.Required(DOMAIN): LIGHT_KNX_SCHEMA, - } + "section_switch": KNXSectionFlat(), + vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), + vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(), + vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(), + vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(), + }, ) - -SWITCH_SCHEMA = vol.Schema( - { - vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, - vol.Required(DOMAIN): { - vol.Optional(CONF_INVERT, default=False): bool, - vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), - vol.Optional(CONF_RESPOND_TO_READ, default=False): bool, - vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, - }, - } -) +KNX_SCHEMA_FOR_PLATFORM = { + Platform.BINARY_SENSOR: BINARY_SENSOR_KNX_SCHEMA, + Platform.COVER: COVER_KNX_SCHEMA, + Platform.LIGHT: LIGHT_KNX_SCHEMA, + Platform.SWITCH: SWITCH_KNX_SCHEMA, +} ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All( vol.Schema( @@ -326,18 +338,16 @@ ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All( cv.key_value_schemas( CONF_PLATFORM, { - Platform.BINARY_SENSOR: vol.Schema( - {vol.Required(CONF_DATA): BINARY_SENSOR_SCHEMA}, extra=vol.ALLOW_EXTRA - ), - Platform.COVER: vol.Schema( - {vol.Required(CONF_DATA): COVER_SCHEMA}, extra=vol.ALLOW_EXTRA - ), - Platform.LIGHT: vol.Schema( - {vol.Required(CONF_DATA): LIGHT_SCHEMA}, extra=vol.ALLOW_EXTRA - ), - Platform.SWITCH: vol.Schema( - {vol.Required(CONF_DATA): SWITCH_SCHEMA}, extra=vol.ALLOW_EXTRA - ), + platform: vol.Schema( + { + vol.Required(CONF_DATA): { + vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, + vol.Required(DOMAIN): knx_schema, + }, + }, + extra=vol.ALLOW_EXTRA, + ) + for platform, knx_schema in KNX_SCHEMA_FOR_PLATFORM.items() }, ), ) diff --git a/homeassistant/components/knx/storage/knx_selector.py b/homeassistant/components/knx/storage/knx_selector.py index fe909f1fd0a..5adf242c16b 100644 --- a/homeassistant/components/knx/storage/knx_selector.py +++ b/homeassistant/components/knx/storage/knx_selector.py @@ -6,11 +6,103 @@ from typing import Any import voluptuous as vol -from ..validation import ga_validator, maybe_ga_validator +from ..validation import ga_validator, maybe_ga_validator, sync_state_validator from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE +from .util import dpt_string_to_dict -class GroupSelect(vol.Any): +class AllSerializeFirst(vol.All): + """Use the first validated value for serialization. + + This is a version of vol.All with custom error handling to + show proper invalid markers for sub-schema items in the UI. + """ + + +class KNXSelectorBase: + """Base class for KNX selectors supporting optional nested schemas.""" + + schema: vol.Schema | vol.Any | vol.All + selector_type: str + # mark if self.schema should be serialized to `schema` key + serialize_subschema: bool = False + + def __call__(self, data: Any) -> Any: + """Validate the passed data.""" + return self.schema(data) + + def serialize(self) -> dict[str, Any]: + """Serialize the selector to a dictionary.""" + # don't use "name", "default", "optional" or "required" in base output + # as it will be overwritten by the parent keys attributes + # "schema" will be overwritten by knx serializer if `self.serialize_subschema` is True + raise NotImplementedError("Subclasses must implement this method.") + + +class KNXSectionFlat(KNXSelectorBase): + """Generate a schema-neutral section with title and description for the following siblings.""" + + selector_type = "knx_section_flat" + schema = vol.Schema(None) + + def __init__( + self, + collapsible: bool = False, + ) -> None: + """Initialize the section.""" + self.collapsible = collapsible + + def serialize(self) -> dict[str, Any]: + """Serialize the selector to a dictionary.""" + return { + "type": self.selector_type, + "collapsible": self.collapsible, + } + + +class KNXSection(KNXSelectorBase): + """Configuration groups similar to DataEntryFlow sections but with more options.""" + + selector_type = "knx_section" + serialize_subschema = True + + def __init__( + self, + schema: dict[str | vol.Marker, vol.Schemable], + collapsible: bool = True, + ) -> None: + """Initialize the section.""" + self.collapsible = collapsible + self.schema = vol.Schema(schema) + + def serialize(self) -> dict[str, Any]: + """Serialize the section to a dictionary.""" + return { + "type": self.selector_type, + "collapsible": self.collapsible, + } + + +class GroupSelectOption(KNXSelectorBase): + """Schema for group select options.""" + + selector_type = "knx_group_select_option" + serialize_subschema: bool = True + + def __init__(self, schema: vol.Schemable, translation_key: str) -> None: + """Initialize the group select option schema.""" + self.translation_key = translation_key + self.schema = vol.Schema(schema) + + def serialize(self) -> dict[str, Any]: + """Serialize the group select option to a dictionary.""" + return { + "type": self.selector_type, + "translation_key": self.translation_key, + } + + +class GroupSelectSchema(vol.Any): """Use the first validated value. This is a version of vol.Any with custom error handling to @@ -35,10 +127,33 @@ class GroupSelect(vol.Any): raise vol.AnyInvalid(self.msg or "no valid value found", path=path) -class GASelector: +class GroupSelect(KNXSelectorBase): + """Selector for group select options.""" + + selector_type = "knx_group_select" + serialize_subschema = True + + def __init__( + self, + *options: GroupSelectOption, + collapsible: bool = True, + ) -> None: + """Initialize the group select selector.""" + self.collapsible = collapsible + self.schema = GroupSelectSchema(*options) + + def serialize(self) -> dict[str, Any]: + """Serialize the group select to a dictionary.""" + return { + "type": self.selector_type, + "collapsible": self.collapsible, + } + + +class GASelector(KNXSelectorBase): """Selector for a KNX group address structure.""" - schema: vol.Schema + selector_type = "knx_group_address" def __init__( self, @@ -48,6 +163,7 @@ class GASelector: write_required: bool = False, state_required: bool = False, dpt: type[Enum] | None = None, + valid_dpt: str | Iterable[str] | None = None, ) -> None: """Initialize the group address selector.""" self.write = write @@ -56,12 +172,35 @@ class GASelector: self.write_required = write_required self.state_required = state_required self.dpt = dpt + # valid_dpt is used in frontend to filter dropdown menu - no validation is done + self.valid_dpt = (valid_dpt,) if isinstance(valid_dpt, str) else valid_dpt self.schema = self.build_schema() - def __call__(self, data: Any) -> Any: - """Validate the passed data.""" - return self.schema(data) + def serialize(self) -> dict[str, Any]: + """Serialize the selector to a dictionary.""" + + options: dict[str, Any] = { + "write": {"required": self.write_required} if self.write else False, + "state": {"required": self.state_required} if self.state else False, + "passive": self.passive, + } + if self.dpt is not None: + options["dptSelect"] = [ + { + "value": item.value, + "translation_key": item.value.replace(".", "_"), + "dpt": dpt_string_to_dict(item.value), # used for filtering GAs + } + for item in self.dpt + ] + if self.valid_dpt is not None: + options["validDPTs"] = [dpt_string_to_dict(dpt) for dpt in self.valid_dpt] + + return { + "type": self.selector_type, + "options": options, + } def build_schema(self) -> vol.Schema: """Create the schema based on configuration.""" @@ -118,3 +257,27 @@ class GASelector: schema[vol.Required(CONF_DPT)] = vol.In({item.value for item in self.dpt}) else: schema[vol.Remove(CONF_DPT)] = object + + +class SyncStateSelector(KNXSelectorBase): + """Selector for knx sync state validation.""" + + schema = vol.Schema(sync_state_validator) + selector_type = "knx_sync_state" + + def __init__(self, allow_false: bool = False) -> None: + """Initialize the sync state validator.""" + self.allow_false = allow_false + + def serialize(self) -> dict[str, Any]: + """Serialize the selector to a dictionary.""" + return { + "type": self.selector_type, + "allow_false": self.allow_false, + } + + def __call__(self, data: Any) -> Any: + """Validate the passed data.""" + if not self.allow_false and not data: + raise vol.Invalid(f"Sync state cannot be {data}") + return self.schema(data) diff --git a/homeassistant/components/knx/storage/serialize.py b/homeassistant/components/knx/storage/serialize.py new file mode 100644 index 00000000000..45b40fb907b --- /dev/null +++ b/homeassistant/components/knx/storage/serialize.py @@ -0,0 +1,47 @@ +"""Custom serializer for KNX schemas.""" + +from typing import Any, cast + +import voluptuous as vol +from voluptuous_serialize import UNSUPPORTED, UnsupportedType, convert + +from homeassistant.const import Platform +from homeassistant.helpers import selector + +from .entity_store_schema import KNX_SCHEMA_FOR_PLATFORM +from .knx_selector import AllSerializeFirst, GroupSelectSchema, KNXSelectorBase + + +def knx_serializer( + schema: vol.Schema, +) -> dict[str, Any] | list[dict[str, Any]] | UnsupportedType: + """Serialize KNX schema.""" + if isinstance(schema, GroupSelectSchema): + return [ + cast( + dict[str, Any], # GroupSelectOption converts to a dict with subschema + convert(option, custom_serializer=knx_serializer), + ) + for option in schema.validators + ] + if isinstance(schema, KNXSelectorBase): + result = schema.serialize() + if schema.serialize_subschema: + result["schema"] = convert(schema.schema, custom_serializer=knx_serializer) + return result + if isinstance(schema, AllSerializeFirst): + return convert(schema.validators[0], custom_serializer=knx_serializer) + + if isinstance(schema, selector.Selector): + return schema.serialize() | {"type": "ha_selector"} + + return UNSUPPORTED + + +def get_serialized_schema( + platform: Platform, +) -> dict[str, Any] | list[dict[str, Any]] | None: + """Get the schema for a specific platform.""" + if knx_schema := KNX_SCHEMA_FOR_PLATFORM.get(platform): + return convert(knx_schema, custom_serializer=knx_serializer) + return None diff --git a/homeassistant/components/knx/storage/util.py b/homeassistant/components/knx/storage/util.py index a3831070a7e..978a0568455 100644 --- a/homeassistant/components/knx/storage/util.py +++ b/homeassistant/components/knx/storage/util.py @@ -3,11 +3,29 @@ from functools import partial from typing import Any +from xknx.typing import DPTMainSubDict + from homeassistant.helpers.typing import ConfigType from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE +def dpt_string_to_dict(dpt: str) -> DPTMainSubDict: + """Convert a DPT string to a typed dictionary with main and sub components. + + Examples: + >>> dpt_string_to_dict("1.010") + {'main': 1, 'sub': 10} + >>> dpt_string_to_dict("5") + {'main': 5, 'sub': None} + """ + dpt_num = dpt.split(".") + return DPTMainSubDict( + main=int(dpt_num[0]), + sub=int(dpt_num[1]) if len(dpt_num) > 1 else None, + ) + + def nested_get(dic: ConfigType, *keys: str, default: Any | None = None) -> Any: """Get the value from a nested dictionary.""" for key in keys: diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 921fc2c5288..d587e02e1a5 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -339,5 +339,275 @@ "name": "[%key:common::action::reload%]", "description": "Reloads the KNX integration." } + }, + "config_panel": { + "entities": { + "create": { + "type_selection": { + "title": "Select entity type", + "header": "Create KNX entity" + }, + "header": "Create new entity", + "_": { + "entity": { + "title": "Entity configuration", + "description": "Home Assistant specific settings.", + "name_title": "Device and entity name", + "name_description": "Define how the entity should be named in Home Assistant.", + "device_description": "A device allows to group multiple entities. Select the device this entity belongs to or create a new one.", + "entity_label": "Entity name", + "entity_description": "Optional if a device is selected, otherwise required. If the entity is assigned to a device, the device name is used as prefix.", + "entity_category_title": "Entity category", + "entity_category_description": "Classification of a non-primary entity. Leave empty for standard behaviour." + }, + "knx": { + "title": "KNX configuration", + "knx_group_address": { + "dpt": "Datapoint type", + "send_address": "Send address", + "state_address": "State address", + "passive_addresses": "Passive addresses", + "valid_dpts": "Valid DPTs" + }, + "sync_state": { + "title": "State updater", + "description": "Actively request state updates from KNX bus for state addresses.", + "strategy": "Strategy", + "options": { + "true": "Use integration default", + "false": "Never", + "init": "Once when connection established", + "expire": "Expire after last value update", + "every": "Scheduled every" + } + } + } + }, + "binary_sensor": { + "description": "Read-only entity for binary datapoints. Window or door states etc.", + "knx": { + "section_binary_sensor": { + "title": "Binary sensor", + "description": "DPT 1 group addresses representing binary states." + }, + "invert": { + "label": "Invert", + "description": "Invert payload before processing." + }, + "section_advanced_options": { + "title": "State properties", + "description": "Properties of the binary sensor state." + }, + "ignore_internal_state": { + "label": "Force update", + "description": "Write each update to the state machine, even if the data is the same." + }, + "context_timeout": { + "label": "Context timeout", + "description": "The time in seconds between multiple identical telegram payloads would count towards an internal counter. This can be used to automate on multi-clicks of a button. `0` to disable this feature." + }, + "reset_after": { + "label": "Reset after", + "description": "Reset back to “off” state after specified seconds." + } + } + }, + "cover": { + "description": "The KNX cover platform is used as an interface to shutter actuators.", + "knx": { + "section_binary_control": { + "title": "Open/Close control", + "description": "DPT 1 group addresses triggering full movement." + }, + "ga_up_down": { + "label": "Open/Close" + }, + "invert_updown": { + "label": "Invert", + "description": "Default is UP (0) to open a cover and DOWN (1) to close a cover. Enable this to invert the open/close commands from/to your KNX actuator." + }, + "section_stop_control": { + "title": "Stop", + "description": "DPT 1 group addresses for stopping movement." + }, + "ga_stop": { + "label": "Stop" + }, + "ga_step": { + "label": "Stepwise move" + }, + "section_position_control": { + "title": "Position", + "description": "DPT 5 group addresses for cover position." + }, + "ga_position_set": { + "label": "Set position" + }, + "ga_position_state": { + "label": "Current position" + }, + "invert_position": { + "label": "Invert", + "description": "Invert payload before processing. Enable if KNX reports 0% as fully closed." + }, + "section_tilt_control": { + "title": "Tilt", + "description": "DPT 5 group addresses for slat tilt angle." + }, + "ga_angle": { + "label": "Tilt angle" + }, + "invert_angle": { + "label": "[%key:component::knx::config_panel::entities::create::cover::knx::invert_position::label%]", + "description": "[%key:component::knx::config_panel::entities::create::cover::knx::invert_position::description%]" + }, + "section_travel_time": { + "title": "Travel time", + "description": "Used to calculate intermediate positions of the cover while traveling." + }, + "travelling_time_up": { + "label": "Travel time for opening", + "description": "Time the cover needs to fully open in seconds." + }, + "travelling_time_down": { + "label": "Travel time for closing", + "description": "Time the cover needs to fully close in seconds." + } + } + }, + "light": { + "description": "The KNX light platform is used as an interface to dimming actuators, LED controllers, DALI gateways and similar.", + "knx": { + "section_switch": { + "title": "Switch", + "description": "Turn the light on/off." + }, + "ga_switch": { + "label": "Switch" + }, + "section_brightness": { + "title": "Brightness", + "description": "Control the brightness of the light." + }, + "ga_brightness": { + "label": "Brightness" + }, + "section_color_temp": { + "title": "Color temperature", + "description": "Control the color temperature of the light." + }, + "ga_color_temp": { + "label": "Color temperature", + "options": { + "5_001": "Percent", + "7_600": "Kelvin", + "9": "2-byte floating point" + } + }, + "color_temp_min": { + "label": "Warmest possible color temperature" + }, + "color_temp_max": { + "label": "Coldest possible color temperature" + }, + "color": { + "title": "Color", + "description": "Control the color of the light.", + "options": { + "single_address": { + "label": "Single address", + "description": "RGB, RGBW or XYY color controlled by a single group address." + }, + "individual_addresses": { + "label": "Individual addresses", + "description": "RGB(W) using individual state and brightness group addresses." + }, + "hsv_addresses": { + "label": "HSV", + "description": "Hue, saturation and brightness using individual group addresses." + } + }, + "ga_color": { + "label": "Color", + "options": { + "232_600": "RGB", + "242_600": "XYY", + "251_600": "RGBW" + } + }, + "section_red": { + "title": "Red", + "description": "Controls the light's red color component. Brightness group address is required." + }, + "ga_red_switch": { + "label": "Red switch" + }, + "ga_red_brightness": { + "label": "Red brightness" + }, + "section_green": { + "title": "Green", + "description": "Controls the light's green color component. Brightness group address is required." + }, + "ga_green_switch": { + "label": "Green switch" + }, + "ga_green_brightness": { + "label": "Green brightness" + }, + "section_blue": { + "title": "Blue", + "description": "Controls the light's blue color component. Brightness group address is required." + }, + "ga_blue_switch": { + "label": "Blue switch" + }, + "ga_blue_brightness": { + "label": "Blue brightness" + }, + "section_white": { + "title": "White", + "description": "Controls the light's white color component. Brightness group address is required." + }, + "ga_white_switch": { + "label": "White switch" + }, + "ga_white_brightness": { + "label": "White brightness" + }, + "ga_hue": { + "label": "Hue", + "description": "Controls the light's hue." + }, + "ga_saturation": { + "label": "Saturation", + "description": "Controls the light's saturation." + } + } + } + }, + "switch": { + "description": "The KNX switch platform is used as an interface to switching actuators.", + "knx": { + "section_switch": { + "title": "Switching", + "description": "DPT 1 group addresses controlling the switch function." + }, + "ga_switch": { + "label": "Switch", + "description": "Group address to switch the device on/off." + }, + "invert": { + "label": "Invert", + "description": "Invert payloads before processing or sending." + }, + "respond_to_read": { + "label": "Respond to read", + "description": "Respond to GroupValueRead telegrams received to the configured send address." + } + } + } + } + } } } diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index b40dc2246b8..387a6e9e6de 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -14,14 +14,14 @@ from xknxproject.exceptions import XknxProjectException from homeassistant.components import panel_custom, websocket_api from homeassistant.components.http import StaticPathConfig -from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM +from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.ulid import ulid_now -from .const import DOMAIN, KNX_MODULE_KEY +from .const import DOMAIN, KNX_MODULE_KEY, SUPPORTED_PLATFORMS_UI from .storage.config_store import ConfigStoreException from .storage.const import CONF_DATA from .storage.entity_store_schema import ( @@ -33,6 +33,7 @@ from .storage.entity_store_validation import ( EntityStoreValidationSuccess, validate_entity_data, ) +from .storage.serialize import get_serialized_schema from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict if TYPE_CHECKING: @@ -43,7 +44,7 @@ URL_BASE: Final = "/knx_static" async def register_panel(hass: HomeAssistant) -> None: """Register the KNX Panel and Websocket API.""" - websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_get_base_data) websocket_api.async_register_command(hass, ws_project_file_process) websocket_api.async_register_command(hass, ws_project_file_remove) websocket_api.async_register_command(hass, ws_group_monitor_info) @@ -57,6 +58,7 @@ async def register_panel(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_get_entity_config) websocket_api.async_register_command(hass, ws_get_entity_entries) websocket_api.async_register_command(hass, ws_create_device) + websocket_api.async_register_command(hass, ws_get_schema) if DOMAIN not in hass.data.get("frontend_panels", {}): await hass.http.async_register_static_paths( @@ -154,12 +156,12 @@ def provide_knx( @websocket_api.require_admin @websocket_api.websocket_command( { - vol.Required("type"): "knx/info", + vol.Required("type"): "knx/get_base_data", } ) @provide_knx @callback -def ws_info( +def ws_get_base_data( hass: HomeAssistant, knx: KNXModule, connection: websocket_api.ActiveConnection, @@ -174,14 +176,18 @@ def ws_info( "tool_version": project_info["tool_version"], "xknxproject_version": project_info["xknxproject_version"], } + connection_info = { + "version": knx.xknx.version, + "connected": knx.xknx.connection_manager.connected.is_set(), + "current_address": str(knx.xknx.current_address), + } connection.send_result( msg["id"], { - "version": knx.xknx.version, - "connected": knx.xknx.connection_manager.connected.is_set(), - "current_address": str(knx.xknx.current_address), - "project": _project_info, + "connection_info": connection_info, + "project_info": _project_info, + "supported_platforms": sorted(SUPPORTED_PLATFORMS_UI), }, ) @@ -204,10 +210,7 @@ async def ws_get_knx_project( knxproject = await knx.project.get_knxproject() connection.send_result( msg["id"], - { - "project_loaded": knx.project.loaded, - "knxproject": knxproject, - }, + knxproject, ) @@ -363,6 +366,28 @@ def ws_validate_entity( ) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "knx/get_schema", + vol.Required(CONF_PLATFORM): vol.Coerce(Platform), + } +) +@websocket_api.async_response +async def ws_get_schema( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +) -> None: + """Provide serialized schema for platform.""" + if schema := get_serialized_schema(msg[CONF_PLATFORM]): + connection.send_result(msg["id"], schema) + return + connection.send_error( + msg["id"], websocket_api.const.ERR_HOME_ASSISTANT_ERROR, "Unknown platform" + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index 1499dd02900..c6f3c2312c0 100644 --- a/homeassistant/components/lacrosse_view/coordinator.py +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -42,7 +42,6 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): self.last_update = time() self.username = entry.data["username"] self.password = entry.data["password"] - self.hass = hass self.name = entry.data["name"] self.id = entry.data["id"] super().__init__( diff --git a/homeassistant/components/launch_library/strings.json b/homeassistant/components/launch_library/strings.json index a587544f836..219d71600bc 100644 --- a/homeassistant/components/launch_library/strings.json +++ b/homeassistant/components/launch_library/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Do you want to configure the Launch Library?" + "description": "Do you want to configure Launch Library?" } } }, diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 234178d3e3b..78acba31afd 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["pypck"], "quality_scale": "bronze", - "requirements": ["pypck==0.8.10", "lcn-frontend==0.2.6"] + "requirements": ["pypck==0.8.10", "lcn-frontend==0.2.7"] } diff --git a/homeassistant/components/letpot/__init__.py b/homeassistant/components/letpot/__init__.py index 4b84a023675..7bcb04b2b4d 100644 --- a/homeassistant/components/letpot/__init__.py +++ b/homeassistant/components/letpot/__init__.py @@ -25,6 +25,7 @@ from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.TIME, diff --git a/homeassistant/components/letpot/icons.json b/homeassistant/components/letpot/icons.json index 43541b57150..1f5e79b04dd 100644 --- a/homeassistant/components/letpot/icons.json +++ b/homeassistant/components/letpot/icons.json @@ -20,6 +20,23 @@ } } }, + "select": { + "display_temperature_unit": { + "default": "mdi:thermometer-lines" + }, + "light_brightness": { + "default": "mdi:brightness-6", + "state": { + "high": "mdi:brightness-7" + } + }, + "light_mode": { + "default": "mdi:sprout", + "state": { + "flower": "mdi:flower" + } + } + }, "sensor": { "water_level": { "default": "mdi:water-percent" diff --git a/homeassistant/components/letpot/manifest.json b/homeassistant/components/letpot/manifest.json index 1397775b351..26f7588033c 100644 --- a/homeassistant/components/letpot/manifest.json +++ b/homeassistant/components/letpot/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["letpot"], "quality_scale": "bronze", - "requirements": ["letpot==0.6.1"] + "requirements": ["letpot==0.6.2"] } diff --git a/homeassistant/components/letpot/select.py b/homeassistant/components/letpot/select.py new file mode 100644 index 00000000000..0a9f6b07046 --- /dev/null +++ b/homeassistant/components/letpot/select.py @@ -0,0 +1,163 @@ +"""Support for LetPot select entities.""" + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from letpot.deviceclient import LetPotDeviceClient +from letpot.models import DeviceFeature, LightMode, TemperatureUnit + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator +from .entity import LetPotEntity, LetPotEntityDescription, exception_handler + +# Each change pushes a 'full' device status with the change. The library will cache +# pending changes to avoid overwriting, but try to avoid a lot of parallelism. +PARALLEL_UPDATES = 1 + + +class LightBrightnessLowHigh(StrEnum): + """Light brightness low/high model.""" + + LOW = "low" + HIGH = "high" + + +def _get_brightness_low_high_value(coordinator: LetPotDeviceCoordinator) -> str | None: + """Return brightness as low/high for a device which only has a low and high value.""" + brightness = coordinator.data.light_brightness + levels = coordinator.device_client.get_light_brightness_levels( + coordinator.device.serial_number + ) + return ( + LightBrightnessLowHigh.LOW.value + if levels[0] == brightness + else LightBrightnessLowHigh.HIGH.value + ) + + +async def _set_brightness_low_high_value( + device_client: LetPotDeviceClient, serial: str, option: str +) -> None: + """Set brightness from low/high for a device which only has a low and high value.""" + levels = device_client.get_light_brightness_levels(serial) + await device_client.set_light_brightness( + serial, levels[0] if option == LightBrightnessLowHigh.LOW.value else levels[1] + ) + + +@dataclass(frozen=True, kw_only=True) +class LetPotSelectEntityDescription(LetPotEntityDescription, SelectEntityDescription): + """Describes a LetPot select entity.""" + + value_fn: Callable[[LetPotDeviceCoordinator], str | None] + set_value_fn: Callable[[LetPotDeviceClient, str, str], Coroutine[Any, Any, None]] + + +SELECTORS: tuple[LetPotSelectEntityDescription, ...] = ( + LetPotSelectEntityDescription( + key="display_temperature_unit", + translation_key="display_temperature_unit", + options=[x.name.lower() for x in TemperatureUnit], + value_fn=( + lambda coordinator: coordinator.data.temperature_unit.name.lower() + if coordinator.data.temperature_unit is not None + else None + ), + set_value_fn=( + lambda device_client, serial, option: device_client.set_temperature_unit( + serial, TemperatureUnit[option.upper()] + ) + ), + supported_fn=( + lambda coordinator: DeviceFeature.TEMPERATURE_SET_UNIT + in coordinator.device_client.device_info( + coordinator.device.serial_number + ).features + ), + entity_category=EntityCategory.CONFIG, + ), + LetPotSelectEntityDescription( + key="light_brightness_low_high", + translation_key="light_brightness", + options=[ + LightBrightnessLowHigh.LOW.value, + LightBrightnessLowHigh.HIGH.value, + ], + value_fn=_get_brightness_low_high_value, + set_value_fn=_set_brightness_low_high_value, + supported_fn=( + lambda coordinator: DeviceFeature.LIGHT_BRIGHTNESS_LOW_HIGH + in coordinator.device_client.device_info( + coordinator.device.serial_number + ).features + ), + entity_category=EntityCategory.CONFIG, + ), + LetPotSelectEntityDescription( + key="light_mode", + translation_key="light_mode", + options=[x.name.lower() for x in LightMode], + value_fn=( + lambda coordinator: coordinator.data.light_mode.name.lower() + if coordinator.data.light_mode is not None + else None + ), + set_value_fn=( + lambda device_client, serial, option: device_client.set_light_mode( + serial, LightMode[option.upper()] + ) + ), + entity_category=EntityCategory.CONFIG, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LetPotConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up LetPot select entities based on a config entry and device status/features.""" + coordinators = entry.runtime_data + async_add_entities( + LetPotSelectEntity(coordinator, description) + for description in SELECTORS + for coordinator in coordinators + if description.supported_fn(coordinator) + ) + + +class LetPotSelectEntity(LetPotEntity, SelectEntity): + """Defines a LetPot select entity.""" + + entity_description: LetPotSelectEntityDescription + + def __init__( + self, + coordinator: LetPotDeviceCoordinator, + description: LetPotSelectEntityDescription, + ) -> None: + """Initialize LetPot select entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{coordinator.device.serial_number}_{description.key}" + + @property + def current_option(self) -> str | None: + """Return the selected entity option.""" + return self.entity_description.value_fn(self.coordinator) + + @exception_handler + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + return await self.entity_description.set_value_fn( + self.coordinator.device_client, + self.coordinator.device.serial_number, + option, + ) diff --git a/homeassistant/components/letpot/strings.json b/homeassistant/components/letpot/strings.json index cdc5a36a15f..6ebd79edf5d 100644 --- a/homeassistant/components/letpot/strings.json +++ b/homeassistant/components/letpot/strings.json @@ -49,6 +49,29 @@ "name": "Refill error" } }, + "select": { + "display_temperature_unit": { + "name": "Temperature unit on display", + "state": { + "celsius": "Celsius", + "fahrenheit": "Fahrenheit" + } + }, + "light_brightness": { + "name": "Light brightness", + "state": { + "low": "[%key:common::state::low%]", + "high": "[%key:common::state::high%]" + } + }, + "light_mode": { + "name": "Light mode", + "state": { + "flower": "Fruits/Flowers", + "vegetable": "Veggies/Herbs" + } + } + }, "sensor": { "water_level": { "name": "Water level" diff --git a/homeassistant/components/lg_thinq/climate.py b/homeassistant/components/lg_thinq/climate.py index 4810336c6e0..67539cbee1e 100644 --- a/homeassistant/components/lg_thinq/climate.py +++ b/homeassistant/components/lg_thinq/climate.py @@ -12,6 +12,7 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + FAN_MEDIUM, PRESET_NONE, SWING_OFF, SWING_ON, @@ -64,6 +65,12 @@ STR_TO_SWING = { SWING_TO_STR = {v: k for k, v in STR_TO_SWING.items()} +STR_TO_HA_FAN: dict[str, str] = { + "mid": FAN_MEDIUM, +} + +HA_FAN_TO_STR = {v: k for k, v in STR_TO_HA_FAN.items()} + _LOGGER = logging.getLogger(__name__) @@ -124,7 +131,9 @@ class ThinQClimateEntity(ThinQEntity, ClimateEntity): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE # Set up fan modes. - self._attr_fan_modes = self.data.fan_modes + self._attr_fan_modes = [ + STR_TO_HA_FAN.get(fan, fan) for fan in self.data.fan_modes + ] if self.fan_modes: self._attr_supported_features |= ClimateEntityFeature.FAN_MODE @@ -148,7 +157,9 @@ class ThinQClimateEntity(ThinQEntity, ClimateEntity): # Update fan, hvac and preset mode. if self.supported_features & ClimateEntityFeature.FAN_MODE: - self._attr_fan_mode = self.data.fan_mode + self._attr_fan_mode = STR_TO_HA_FAN.get( + self.data.fan_mode, self.data.fan_mode + ) if self.supported_features & ClimateEntityFeature.SWING_MODE: self._attr_swing_mode = STR_TO_SWING.get(self.data.swing_mode) if self.supported_features & ClimateEntityFeature.SWING_HORIZONTAL_MODE: @@ -266,7 +277,10 @@ class ThinQClimateEntity(ThinQEntity, ClimateEntity): fan_mode, ) await self.async_call_api( - self.coordinator.api.async_set_fan_mode(self.property_id, fan_mode) + self.coordinator.api.async_set_fan_mode( + self.property_id, + HA_FAN_TO_STR.get(fan_mode, fan_mode), + ) ) async def async_set_swing_mode(self, swing_mode: str) -> None: diff --git a/homeassistant/components/lg_thinq/coordinator.py b/homeassistant/components/lg_thinq/coordinator.py index 9f84c422277..ffdde3188db 100644 --- a/homeassistant/components/lg_thinq/coordinator.py +++ b/homeassistant/components/lg_thinq/coordinator.py @@ -37,7 +37,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): name=f"{DOMAIN}_{ha_bridge.device.device_id}", ) - self.data = {} + self.data = ha_bridge.update_status(None) self.api = ha_bridge self.device_id = ha_bridge.device.device_id self.sub_id = ha_bridge.sub_id diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index 303660aef75..f7001a92b9d 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -45,6 +45,9 @@ }, "display_light": { "default": "mdi:lightbulb-on-outline" + }, + "air_clean_operation_mode": { + "default": "mdi:air-filter" } }, "binary_sensor": { @@ -92,7 +95,7 @@ "state": { "slow": "mdi:fan-chevron-down", "low": "mdi:fan-speed-1", - "mid": "mdi:fan-speed-2", + "medium": "mdi:fan-speed-2", "high": "mdi:fan-speed-3", "power": "mdi:fan-chevron-up", "auto": "mdi:fan-auto" diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index d0972a80127..52b9ea4a346 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -70,6 +70,9 @@ }, "display_light": { "name": "Lighting" + }, + "air_clean_operation_mode": { + "name": "[%key:component::lg_thinq::entity::climate::climate_air_conditioner::state_attributes::preset_mode::state::air_clean%]" } }, "binary_sensor": { @@ -120,7 +123,7 @@ "state": { "slow": "Slow", "low": "[%key:common::state::low%]", - "mid": "[%key:common::state::medium%]", + "medium": "[%key:common::state::medium%]", "high": "[%key:common::state::high%]", "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]", "auto": "[%key:common::state::auto%]" @@ -178,7 +181,7 @@ "no_battery_error": "Robot cleaner's battery is low", "no_dust_bin_error": "Dust bin is not installed", "no_filter_error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::filter_clogging_error%]", - "out_of_balance_error": "Out of balance load", + "out_of_balance_error": "Out-of-balance load", "overfill_error": "Overfill error", "part_malfunction_error": "AIE error", "power_code_connection_error": "Power cord connection error", @@ -220,7 +223,7 @@ "error_during_cleaning": "Cleaning stopped due to an error", "error_during_washing": "An error has occurred in the washing machine", "error_has_occurred": "An error has occurred", - "frozen_is_complete": "Ice plus is done", + "frozen_is_complete": "Ice Plus is done", "homeguard_is_stopped": "Home Guard has stopped", "lack_of_water": "There is no water in the water tank", "motion_is_detected": "Photograph is sent as movement is detected during Home Guard", diff --git a/homeassistant/components/lg_thinq/switch.py b/homeassistant/components/lg_thinq/switch.py index 06363140193..8ba680ca93d 100644 --- a/homeassistant/components/lg_thinq/switch.py +++ b/homeassistant/components/lg_thinq/switch.py @@ -31,6 +31,15 @@ class ThinQSwitchEntityDescription(SwitchEntityDescription): off_key: str | None = None +DRYER_OPERATION_SWITCH_DESC = ThinQSwitchEntityDescription( + key=ThinQProperty.DRYER_OPERATION_MODE, translation_key="operation_power" +) + +WASHER_OPERATION_SWITCH_DESC = ThinQSwitchEntityDescription( + key=ThinQProperty.WASHER_OPERATION_MODE, translation_key="operation_power" +) + + DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ...]] = { DeviceType.AIR_CONDITIONER: ( ThinQSwitchEntityDescription( @@ -52,6 +61,13 @@ DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ... off_key="false", entity_category=EntityCategory.CONFIG, ), + ThinQSwitchEntityDescription( + key=ThinQProperty.AIR_CLEAN_OPERATION_MODE, + translation_key=ThinQProperty.AIR_CLEAN_OPERATION_MODE, + on_key="on", + off_key="off", + entity_category=EntityCategory.CONFIG, + ), ), DeviceType.AIR_PURIFIER_FAN: ( ThinQSwitchEntityDescription( @@ -84,6 +100,13 @@ DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ... translation_key="operation_power", ), ), + DeviceType.DISH_WASHER: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.DISH_WASHER_OPERATION_MODE, + translation_key="operation_power", + ), + ), + DeviceType.DRYER: (DRYER_OPERATION_SWITCH_DESC,), DeviceType.HUMIDIFIER: ( ThinQSwitchEntityDescription( key=ThinQProperty.HUMIDIFIER_OPERATION_MODE, @@ -155,6 +178,27 @@ DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ... entity_category=EntityCategory.CONFIG, ), ), + DeviceType.STYLER: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.STYLER_OPERATION_MODE, translation_key="operation_power" + ), + ), + DeviceType.VENTILATOR: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.VENTILATOR_OPERATION_MODE, + translation_key="operation_power", + entity_category=EntityCategory.CONFIG, + ), + ), + DeviceType.WASHCOMBO_MAIN: (WASHER_OPERATION_SWITCH_DESC,), + DeviceType.WASHCOMBO_MINI: (WASHER_OPERATION_SWITCH_DESC,), + DeviceType.WASHER: (WASHER_OPERATION_SWITCH_DESC,), + DeviceType.WASHTOWER: ( + DRYER_OPERATION_SWITCH_DESC, + WASHER_OPERATION_SWITCH_DESC, + ), + DeviceType.WASHTOWER_DRYER: (DRYER_OPERATION_SWITCH_DESC,), + DeviceType.WASHTOWER_WASHER: (WASHER_OPERATION_SWITCH_DESC,), DeviceType.WINE_CELLAR: ( ThinQSwitchEntityDescription( key=ThinQProperty.OPTIMAL_HUMIDITY, @@ -186,7 +230,8 @@ async def async_setup_entry( entities.extend( ThinQSwitchEntity(coordinator, description, property_id) for property_id in coordinator.api.get_active_idx( - description.key, ActiveMode.READ_WRITE + description.key, + ActiveMode.WRITABLE, ) ) diff --git a/homeassistant/components/linear_garage_door/__init__.py b/homeassistant/components/linear_garage_door/__init__.py deleted file mode 100644 index a80aa99628b..00000000000 --- a/homeassistant/components/linear_garage_door/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -"""The Linear Garage Door integration.""" - -from __future__ import annotations - -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from .const import DOMAIN -from .coordinator import LinearConfigEntry, LinearUpdateCoordinator - -PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT] - - -async def async_setup_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> bool: - """Set up Linear Garage Door from a config entry.""" - - ir.async_create_issue( - hass, - DOMAIN, - DOMAIN, - breaks_in_ha_version="2025.8.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_integration", - translation_placeholders={ - "nice_go": "https://www.home-assistant.io/integrations/linear_garage_door", - "entries": "/config/integrations/integration/linear_garage_door", - }, - ) - - coordinator = LinearUpdateCoordinator(hass, entry) - - await coordinator.async_config_entry_first_refresh() - - entry.runtime_data = coordinator - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> bool: - """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def async_remove_entry(hass: HomeAssistant, entry: LinearConfigEntry) -> None: - """Remove a config entry.""" - if not hass.config_entries.async_loaded_entries(DOMAIN): - ir.async_delete_issue(hass, DOMAIN, DOMAIN) - # Remove any remaining disabled or ignored entries - for _entry in hass.config_entries.async_entries(DOMAIN): - hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id)) diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py deleted file mode 100644 index 2cfd0af6a8f..00000000000 --- a/homeassistant/components/linear_garage_door/config_flow.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Config flow for Linear Garage Door integration.""" - -from __future__ import annotations - -from collections.abc import Collection, Mapping, Sequence -import logging -from typing import Any -import uuid - -from linear_garage_door import Linear -from linear_garage_door.errors import InvalidLoginError -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -STEP_USER_DATA_SCHEMA = { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, -} - - -async def validate_input( - hass: HomeAssistant, - data: dict[str, str], -) -> dict[str, Sequence[Collection[str]]]: - """Validate the user input allows us to connect. - - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - - hub = Linear() - - device_id = str(uuid.uuid4()) - try: - await hub.login( - data["email"], - data["password"], - device_id=device_id, - client_session=async_get_clientsession(hass), - ) - - sites = await hub.get_sites() - except InvalidLoginError as err: - raise InvalidAuth from err - finally: - await hub.close() - - return { - "email": data["email"], - "password": data["password"], - "sites": sites, - "device_id": device_id, - } - - -class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Linear Garage Door.""" - - VERSION = 1 - - def __init__(self) -> None: - """Initialize the config flow.""" - self.data: dict[str, Sequence[Collection[str]]] = {} - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle the initial step.""" - data_schema = vol.Schema(STEP_USER_DATA_SCHEMA) - - if user_input is None: - return self.async_show_form(step_id="user", data_schema=data_schema) - - errors = {} - - try: - info = await validate_input(self.hass, user_input) - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - self.data = info - - # Check if we are reauthenticating - if self.source == SOURCE_REAUTH: - return self.async_update_reload_and_abort( - self._get_reauth_entry(), - data_updates={ - CONF_EMAIL: self.data["email"], - CONF_PASSWORD: self.data["password"], - }, - ) - - return await self.async_step_site() - - return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors - ) - - async def async_step_site( - self, - user_input: dict[str, Any] | None = None, - ) -> ConfigFlowResult: - """Handle the site step.""" - - if isinstance(self.data["sites"], list): - sites: list[dict[str, str]] = self.data["sites"] - - if not user_input: - return self.async_show_form( - step_id="site", - data_schema=vol.Schema( - { - vol.Required("site"): vol.In( - {site["id"]: site["name"] for site in sites} - ) - } - ), - ) - - site_id = user_input["site"] - - site_name = next(site["name"] for site in sites if site["id"] == site_id) - - await self.async_set_unique_id(site_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=site_name, - data={ - "site_id": site_id, - "email": self.data["email"], - "password": self.data["password"], - "device_id": self.data["device_id"], - }, - ) - - async def async_step_reauth( - self, entry_data: Mapping[str, Any] - ) -> ConfigFlowResult: - """Reauth in case of a password change or other error.""" - return await self.async_step_user() - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" - - -class InvalidDeviceID(HomeAssistantError): - """Error to indicate there is invalid device ID.""" diff --git a/homeassistant/components/linear_garage_door/const.py b/homeassistant/components/linear_garage_door/const.py deleted file mode 100644 index 7b3625c7c67..00000000000 --- a/homeassistant/components/linear_garage_door/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the Linear Garage Door integration.""" - -DOMAIN = "linear_garage_door" diff --git a/homeassistant/components/linear_garage_door/coordinator.py b/homeassistant/components/linear_garage_door/coordinator.py deleted file mode 100644 index 3844e1ae7de..00000000000 --- a/homeassistant/components/linear_garage_door/coordinator.py +++ /dev/null @@ -1,86 +0,0 @@ -"""DataUpdateCoordinator for Linear.""" - -from __future__ import annotations - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from datetime import timedelta -import logging -from typing import Any, cast - -from linear_garage_door import Linear -from linear_garage_door.errors import InvalidLoginError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) - -type LinearConfigEntry = ConfigEntry[LinearUpdateCoordinator] - - -@dataclass -class LinearDevice: - """Linear device dataclass.""" - - name: str - subdevices: dict[str, dict[str, str]] - - -class LinearUpdateCoordinator(DataUpdateCoordinator[dict[str, LinearDevice]]): - """DataUpdateCoordinator for Linear.""" - - _devices: list[dict[str, Any]] | None = None - config_entry: LinearConfigEntry - - def __init__(self, hass: HomeAssistant, config_entry: LinearConfigEntry) -> None: - """Initialize DataUpdateCoordinator for Linear.""" - super().__init__( - hass, - _LOGGER, - config_entry=config_entry, - name="Linear Garage Door", - update_interval=timedelta(seconds=60), - ) - self.site_id = config_entry.data["site_id"] - - async def _async_update_data(self) -> dict[str, LinearDevice]: - """Get the data for Linear.""" - - async def update_data(linear: Linear) -> dict[str, Any]: - if not self._devices: - self._devices = await linear.get_devices(self.site_id) - - data = {} - - for device in self._devices: - device_id = str(device["id"]) - state = await linear.get_device_state(device_id) - data[device_id] = LinearDevice(cast(str, device["name"]), state) - return data - - return await self.execute(update_data) - - async def execute[_T](self, func: Callable[[Linear], Awaitable[_T]]) -> _T: - """Execute an API call.""" - linear = Linear() - try: - await linear.login( - email=self.config_entry.data["email"], - password=self.config_entry.data["password"], - device_id=self.config_entry.data["device_id"], - client_session=async_get_clientsession(self.hass), - ) - except InvalidLoginError as err: - if ( - str(err) - == "Login error: Login provided is invalid, please check the email and password" - ): - raise ConfigEntryAuthFailed from err - raise ConfigEntryNotReady from err - result = await func(linear) - await linear.close() - return result diff --git a/homeassistant/components/linear_garage_door/cover.py b/homeassistant/components/linear_garage_door/cover.py deleted file mode 100644 index 1f6c0999531..00000000000 --- a/homeassistant/components/linear_garage_door/cover.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Cover entity for Linear Garage Doors.""" - -from datetime import timedelta -from typing import Any - -from homeassistant.components.cover import ( - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .coordinator import LinearConfigEntry -from .entity import LinearEntity - -SUPPORTED_SUBDEVICES = ["GDO"] -PARALLEL_UPDATES = 1 -SCAN_INTERVAL = timedelta(seconds=10) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: LinearConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up Linear Garage Door cover.""" - coordinator = config_entry.runtime_data - - async_add_entities( - LinearCoverEntity(coordinator, device_id, device_data.name, sub_device_id) - for device_id, device_data in coordinator.data.items() - for sub_device_id in device_data.subdevices - if sub_device_id in SUPPORTED_SUBDEVICES - ) - - -class LinearCoverEntity(LinearEntity, CoverEntity): - """Representation of a Linear cover.""" - - _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - _attr_name = None - _attr_device_class = CoverDeviceClass.GARAGE - - @property - def is_closed(self) -> bool: - """Return if cover is closed.""" - return self.sub_device.get("Open_B") == "false" - - @property - def is_opened(self) -> bool: - """Return if cover is open.""" - return self.sub_device.get("Open_B") == "true" - - @property - def is_opening(self) -> bool: - """Return if cover is opening.""" - return self.sub_device.get("Opening_P") == "0" - - @property - def is_closing(self) -> bool: - """Return if cover is closing.""" - return self.sub_device.get("Opening_P") == "100" - - async def async_close_cover(self, **kwargs: Any) -> None: - """Close the garage door.""" - if self.is_closed: - return - - await self.coordinator.execute( - lambda linear: linear.operate_device( - self._device_id, self._sub_device_id, "Close" - ) - ) - - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the garage door.""" - if self.is_opened: - return - - await self.coordinator.execute( - lambda linear: linear.operate_device( - self._device_id, self._sub_device_id, "Open" - ) - ) diff --git a/homeassistant/components/linear_garage_door/diagnostics.py b/homeassistant/components/linear_garage_door/diagnostics.py deleted file mode 100644 index ff5ca5639bf..00000000000 --- a/homeassistant/components/linear_garage_door/diagnostics.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Diagnostics support for Linear Garage Door.""" - -from __future__ import annotations - -from dataclasses import asdict -from typing import Any - -from homeassistant.components.diagnostics import async_redact_data -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant - -from .coordinator import LinearConfigEntry - -TO_REDACT = {CONF_PASSWORD, CONF_EMAIL} - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: LinearConfigEntry -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - coordinator = entry.runtime_data - - return { - "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "coordinator_data": { - device_id: asdict(device_data) - for device_id, device_data in coordinator.data.items() - }, - } diff --git a/homeassistant/components/linear_garage_door/entity.py b/homeassistant/components/linear_garage_door/entity.py deleted file mode 100644 index a7adf95f82e..00000000000 --- a/homeassistant/components/linear_garage_door/entity.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Base entity for Linear.""" - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import LinearDevice, LinearUpdateCoordinator - - -class LinearEntity(CoordinatorEntity[LinearUpdateCoordinator]): - """Common base for Linear entities.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: LinearUpdateCoordinator, - device_id: str, - device_name: str, - sub_device_id: str, - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - - self._attr_unique_id = f"{device_id}-{sub_device_id}" - self._device_id = device_id - self._sub_device_id = sub_device_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_id)}, - name=device_name, - manufacturer="Linear", - model="Garage Door Opener", - ) - - @property - def linear_device(self) -> LinearDevice: - """Return the Linear device.""" - return self.coordinator.data[self._device_id] - - @property - def sub_device(self) -> dict[str, str]: - """Return the subdevice.""" - return self.linear_device.subdevices[self._sub_device_id] diff --git a/homeassistant/components/linear_garage_door/light.py b/homeassistant/components/linear_garage_door/light.py deleted file mode 100644 index 59243817fbb..00000000000 --- a/homeassistant/components/linear_garage_door/light.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Linear garage door light.""" - -from typing import Any - -from linear_garage_door import Linear - -from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .coordinator import LinearConfigEntry -from .entity import LinearEntity - -SUPPORTED_SUBDEVICES = ["Light"] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: LinearConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Set up Linear Garage Door cover.""" - coordinator = config_entry.runtime_data - data = coordinator.data - - async_add_entities( - LinearLightEntity( - device_id=device_id, - device_name=data[device_id].name, - sub_device_id=subdev, - coordinator=coordinator, - ) - for device_id in data - for subdev in data[device_id].subdevices - if subdev in SUPPORTED_SUBDEVICES - ) - - -class LinearLightEntity(LinearEntity, LightEntity): - """Light for Linear devices.""" - - _attr_color_mode = ColorMode.BRIGHTNESS - _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - _attr_translation_key = "light" - - @property - def is_on(self) -> bool: - """Return if the light is on or not.""" - return bool(self.sub_device["On_B"] == "true") - - @property - def brightness(self) -> int | None: - """Return the brightness of the light.""" - return round(int(self.sub_device["On_P"]) / 100 * 255) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the light.""" - - async def _turn_on(linear: Linear) -> None: - """Turn on the light.""" - if not kwargs: - await linear.operate_device(self._device_id, self._sub_device_id, "On") - elif ATTR_BRIGHTNESS in kwargs: - brightness = round((kwargs[ATTR_BRIGHTNESS] / 255) * 100) - await linear.operate_device( - self._device_id, self._sub_device_id, f"DimPercent:{brightness}" - ) - - await self.coordinator.execute(_turn_on) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the light.""" - - await self.coordinator.execute( - lambda linear: linear.operate_device( - self._device_id, self._sub_device_id, "Off" - ) - ) diff --git a/homeassistant/components/linear_garage_door/manifest.json b/homeassistant/components/linear_garage_door/manifest.json deleted file mode 100644 index f1eb4302cf0..00000000000 --- a/homeassistant/components/linear_garage_door/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "linear_garage_door", - "name": "Linear Garage Door", - "codeowners": ["@IceBotYT"], - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/linear_garage_door", - "iot_class": "cloud_polling", - "requirements": ["linear-garage-door==0.2.9"] -} diff --git a/homeassistant/components/linear_garage_door/strings.json b/homeassistant/components/linear_garage_door/strings.json deleted file mode 100644 index 40ffcf22e8d..00000000000 --- a/homeassistant/components/linear_garage_door/strings.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]" - } - } - }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } - }, - "entity": { - "light": { - "light": { - "name": "[%key:component::light::title%]" - } - } - }, - "issues": { - "deprecated_integration": { - "title": "The Linear Garage Door integration will be removed", - "description": "The Linear Garage Door integration will be removed as it has been replaced by the [Nice G.O.]({nice_go}) integration. Please migrate to the new integration.\n\nTo resolve this issue, please remove all Linear Garage Door entries from your configuration and add the new Nice G.O. integration. [Click here to see your existing Linear Garage Door integration entries]({entries})." - } - } -} diff --git a/homeassistant/components/livisi/coordinator.py b/homeassistant/components/livisi/coordinator.py index 8d490dca952..1339ae7d68c 100644 --- a/homeassistant/components/livisi/coordinator.py +++ b/homeassistant/components/livisi/coordinator.py @@ -45,7 +45,6 @@ class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): name="Livisi devices", update_interval=timedelta(seconds=DEVICE_POLLING_DELAY), ) - self.hass = hass self.aiolivisi = aiolivisi self.websocket = Websocket(aiolivisi) self.devices: set[str] = set() diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index 6b92032e4ab..cc9634ac1b6 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -91,7 +91,7 @@ async def async_setup_entry( class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): """An aircon or heat pump.""" - _attr_current_humidity: float | None = None # type: ignore[assignment] + _attr_current_humidity: float | None = None _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index cca69969f70..bc6b34ee970 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/lyric", "iot_class": "cloud_polling", "loggers": ["aiolyric"], - "requirements": ["aiolyric==2.0.1"] + "requirements": ["aiolyric==2.0.2"] } diff --git a/homeassistant/components/madvr/manifest.json b/homeassistant/components/madvr/manifest.json index 0ac906fdbef..e45a4c60f30 100644 --- a/homeassistant/components/madvr/manifest.json +++ b/homeassistant/components/madvr/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/madvr", "integration_type": "device", "iot_class": "local_push", - "requirements": ["py-madvr2==1.6.32"] + "requirements": ["py-madvr2==1.6.40"] } diff --git a/homeassistant/components/mastodon/__init__.py b/homeassistant/components/mastodon/__init__.py index 17b8614a2e9..b6e0d863471 100644 --- a/homeassistant/components/mastodon/__init__.py +++ b/homeassistant/components/mastodon/__init__.py @@ -8,12 +8,11 @@ from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET, - CONF_NAME, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -22,7 +21,7 @@ from .coordinator import MastodonConfigEntry, MastodonCoordinator, MastodonData from .services import setup_services from .utils import construct_mastodon_username, create_mastodon_client -PLATFORMS: list[Platform] = [Platform.NOTIFY, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.SENSOR] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -53,26 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> entry.runtime_data = MastodonData(client, instance, account, coordinator) - await discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - {CONF_NAME: entry.title, "client": client}, - {}, - ) - - await hass.config_entries.async_forward_entry_setups( - entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms( - entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_migrate_entry(hass: HomeAssistant, entry: MastodonConfigEntry) -> bool: diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index d7b21ad3a0c..157b2986c4d 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -7,5 +7,6 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["mastodon"], - "requirements": ["Mastodon.py==2.0.1"] + "quality_scale": "bronze", + "requirements": ["Mastodon.py==2.1.2"] } diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py deleted file mode 100644 index 149ef1f6a48..00000000000 --- a/homeassistant/components/mastodon/notify.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Mastodon platform for notify component.""" - -from __future__ import annotations - -from typing import Any, cast - -from mastodon import Mastodon -from mastodon.Mastodon import MastodonAPIError, MediaAttachment -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_DATA, - PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA, - BaseNotificationService, -) -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, issue_registry as ir -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from .const import ( - ATTR_CONTENT_WARNING, - ATTR_MEDIA_WARNING, - CONF_BASE_URL, - DEFAULT_URL, - DOMAIN, -) -from .utils import get_media_type - -ATTR_MEDIA = "media" -ATTR_TARGET = "target" - -PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_BASE_URL, default=DEFAULT_URL): cv.string, - } -) - -INTEGRATION_TITLE = "Mastodon" - - -async def async_get_service( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> MastodonNotificationService | None: - """Get the Mastodon notification service.""" - if discovery_info is None: - return None - - client = cast(Mastodon, discovery_info.get("client")) - - return MastodonNotificationService(hass, client) - - -class MastodonNotificationService(BaseNotificationService): - """Implement the notification service for Mastodon.""" - - def __init__( - self, - hass: HomeAssistant, - client: Mastodon, - ) -> None: - """Initialize the service.""" - - self.client = client - - def send_message(self, message: str = "", **kwargs: Any) -> None: - """Toot a message, with media perhaps.""" - - ir.create_issue( - self.hass, - DOMAIN, - "deprecated_notify_action_mastodon", - breaks_in_ha_version="2025.9.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_notify_action", - ) - - target = None - if (target_list := kwargs.get(ATTR_TARGET)) is not None: - target = cast(list[str], target_list)[0] - - data = kwargs.get(ATTR_DATA) - - media = None - mediadata = None - sensitive = False - content_warning = None - - if data: - media = data.get(ATTR_MEDIA) - if media: - if not self.hass.config.is_allowed_path(media): - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="not_whitelisted_directory", - translation_placeholders={"media": media}, - ) - mediadata = self._upload_media(media) - - sensitive = data.get(ATTR_MEDIA_WARNING) - content_warning = data.get(ATTR_CONTENT_WARNING) - - if mediadata: - try: - self.client.status_post( - message, - visibility=target, - spoiler_text=content_warning, - media_ids=mediadata.id, - sensitive=sensitive, - ) - except MastodonAPIError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="unable_to_send_message", - ) from err - - else: - try: - self.client.status_post( - message, visibility=target, spoiler_text=content_warning - ) - except MastodonAPIError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="unable_to_send_message", - ) from err - - def _upload_media(self, media_path: Any = None) -> MediaAttachment: - """Upload media.""" - with open(media_path, "rb"): - media_type = get_media_type(media_path) - try: - mediadata: MediaAttachment = self.client.media_post( - media_path, mime_type=media_type - ) - except MastodonAPIError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="unable_to_upload_image", - translation_placeholders={"media_path": media_path}, - ) from err - - return mediadata diff --git a/homeassistant/components/mastodon/quality_scale.yaml b/homeassistant/components/mastodon/quality_scale.yaml index f07f7e0a8ad..ff3d4ad3db0 100644 --- a/homeassistant/components/mastodon/quality_scale.yaml +++ b/homeassistant/components/mastodon/quality_scale.yaml @@ -6,10 +6,7 @@ rules: common-modules: done config-flow-test-coverage: done config-flow: done - dependency-transparency: - status: todo - comment: | - Mastodon.py does not have CI build/publish. + dependency-transparency: done docs-actions: done docs-high-level-description: done docs-installation-instructions: done @@ -26,10 +23,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: todo - comment: | - Awaiting legacy Notify deprecation. + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt @@ -39,19 +33,12 @@ rules: entity-unavailable: done integration-owner: done log-when-unavailable: done - parallel-updates: - status: todo - comment: | - Awaiting legacy Notify deprecation. + parallel-updates: done reauthentication-flow: status: todo comment: | Waiting to move to oAuth. - test-coverage: - status: todo - comment: | - Awaiting legacy Notify deprecation. - + test-coverage: done # Gold devices: done diagnostics: done diff --git a/homeassistant/components/mastodon/strings.json b/homeassistant/components/mastodon/strings.json index 9e6cf6db6bf..c37f9b2e941 100644 --- a/homeassistant/components/mastodon/strings.json +++ b/homeassistant/components/mastodon/strings.json @@ -42,12 +42,6 @@ "message": "{media} is not a whitelisted directory." } }, - "issues": { - "deprecated_notify_action": { - "title": "Deprecated Notify action used for Mastodon", - "description": "The Notify action for Mastodon is deprecated.\n\nUse the `mastodon.post` action instead." - } - }, "entity": { "sensor": { "followers": { diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 3ce0cc68012..b36e826e711 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -396,7 +396,7 @@ DISCOVERY_SCHEMAS = [ platform=Platform.BINARY_SENSOR, entity_description=MatterBinarySensorEntityDescription( key="DishwasherAlarmDoorError", - translation_key="dishwasher_alarm_door", + translation_key="alarm_door", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, device_to_ha=lambda x: ( @@ -407,4 +407,72 @@ DISCOVERY_SCHEMAS = [ required_attributes=(clusters.DishwasherAlarm.Attributes.State,), allow_multi=True, ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="ValveConfigurationAndControlValveFault_GeneralFault", + translation_key="valve_fault_general_fault", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + device_to_ha=lambda x: ( + x + == clusters.ValveConfigurationAndControl.Bitmaps.ValveFaultBitmap.kGeneralFault + ), + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.ValveFault, + ), + allow_multi=True, + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="ValveConfigurationAndControlValveFault_Blocked", + translation_key="valve_fault_blocked", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + device_to_ha=lambda x: ( + x + == clusters.ValveConfigurationAndControl.Bitmaps.ValveFaultBitmap.kBlocked + ), + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.ValveFault, + ), + allow_multi=True, + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="ValveConfigurationAndControlValveFault_Leaking", + translation_key="valve_fault_leaking", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + device_to_ha=lambda x: ( + x + == clusters.ValveConfigurationAndControl.Bitmaps.ValveFaultBitmap.kLeaking + ), + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.ValveFault, + ), + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="RefrigeratorAlarmDoorOpen", + translation_key="alarm_door", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + device_to_ha=lambda x: ( + x == clusters.RefrigeratorAlarm.Bitmaps.AlarmBitmap.kDoorOpen + ), + ), + entity_class=MatterBinarySensor, + required_attributes=(clusters.RefrigeratorAlarm.Attributes.State,), + allow_multi=True, + ), ] diff --git a/homeassistant/components/matter/button.py b/homeassistant/components/matter/button.py index 6a0a5fc5b1d..f75c5063c06 100644 --- a/homeassistant/components/matter/button.py +++ b/homeassistant/components/matter/button.py @@ -143,4 +143,16 @@ DISCOVERY_SCHEMAS = [ value_contains=clusters.ActivatedCarbonFilterMonitoring.Commands.ResetCondition.command_id, allow_multi=True, ), + MatterDiscoverySchema( + platform=Platform.BUTTON, + entity_description=MatterButtonEntityDescription( + key="SmokeCoAlarmSelfTestRequest", + translation_key="self_test_request", + entity_category=EntityCategory.DIAGNOSTIC, + command=clusters.SmokeCoAlarm.Commands.SelfTestRequest, + ), + entity_class=MatterCommandButton, + required_attributes=(clusters.SmokeCoAlarm.Attributes.AcceptedCommandList,), + value_contains=clusters.SmokeCoAlarm.Commands.SelfTestRequest.command_id, + ), ] diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index 475504d5aeb..dc1fbc25181 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -17,6 +17,9 @@ }, "stop": { "default": "mdi:stop" + }, + "self_test_request": { + "default": "mdi:refresh-auto" } }, "fan": { @@ -54,6 +57,15 @@ "current_phase": { "default": "mdi:state-machine" }, + "eve_weather_trend": { + "default": "mdi:weather", + "state": { + "sunny": "mdi:weather-sunny", + "cloudy": "mdi:weather-cloudy", + "rainy": "mdi:weather-rainy", + "stormy": "mdi:weather-windy" + } + }, "air_quality": { "default": "mdi:air-filter" }, diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 9db0dfc9881..b79113d422e 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -7,6 +7,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==8.0.0"], + "requirements": ["python-matter-server==8.1.0"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/homeassistant/components/matter/number.py b/homeassistant/components/matter/number.py index d2184891dc1..4540c5bd2b3 100644 --- a/homeassistant/components/matter/number.py +++ b/homeassistant/components/matter/number.py @@ -313,6 +313,23 @@ DISCOVERY_SCHEMAS = [ clusters.OccupancySensing.Attributes.PIROccupiedToUnoccupiedDelay, ), ), + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="ValveConfigurationAndControlDefaultOpenDuration", + entity_category=EntityCategory.CONFIG, + translation_key="valve_configuration_and_control_default_open_duration", + native_max_value=65534, + native_min_value=1, + native_unit_of_measurement=UnitOfTime.SECONDS, + mode=NumberMode.BOX, + ), + entity_class=MatterNumber, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.DefaultOpenDuration, + ), + allow_multi=True, + ), MatterDiscoverySchema( platform=Platform.NUMBER, entity_description=MatterRangeNumberEntityDescription( diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 9e2ef33167b..d8e55b7b1ff 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -32,11 +32,13 @@ from homeassistant.const import ( REVOLUTIONS_PER_MINUTE, EntityCategory, Platform, + UnitOfApparentPower, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, UnitOfPressure, + UnitOfReactivePower, UnitOfTemperature, UnitOfTime, UnitOfVolume, @@ -68,6 +70,14 @@ CONTAMINATION_STATE_MAP = { clusters.SmokeCoAlarm.Enums.ContaminationStateEnum.kCritical: "critical", } +EVE_CLUSTER_WEATHER_MAP = { + # enum with known Weather state values which we can translate + 1: "sunny", + 3: "cloudy", + 6: "rainy", + 14: "stormy", +} + OPERATIONAL_STATE_MAP = { # enum with known Operation state values which we can translate clusters.OperationalState.Enums.OperationalStateEnum.kStopped: "stopped", @@ -515,6 +525,19 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterSensor, required_attributes=(EveCluster.Attributes.Pressure,), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="EveWeatherWeatherTrend", + translation_key="eve_weather_trend", + device_class=SensorDeviceClass.ENUM, + native_unit_of_measurement=None, + options=[x for x in EVE_CLUSTER_WEATHER_MAP.values() if x is not None], + device_to_ha=EVE_CLUSTER_WEATHER_MAP.get, + ), + entity_class=MatterSensor, + required_attributes=(EveCluster.Attributes.WeatherTrend,), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( @@ -782,10 +805,43 @@ DISCOVERY_SCHEMAS = [ clusters.ElectricalPowerMeasurement.Attributes.ActivePower, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ElectricalPowerMeasurementApparentPower", + device_class=SensorDeviceClass.APPARENT_POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfApparentPower.MILLIVOLT_AMPERE, + suggested_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ElectricalPowerMeasurement.Attributes.ApparentPower, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ElectricalPowerMeasurementReactivePower", + device_class=SensorDeviceClass.REACTIVE_POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE, + suggested_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ElectricalPowerMeasurement.Attributes.ReactivePower, + ), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="ElectricalPowerMeasurementVoltage", + translation_key="voltage", device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, @@ -796,10 +852,45 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterSensor, required_attributes=(clusters.ElectricalPowerMeasurement.Attributes.Voltage,), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ElectricalPowerMeasurementRMSVoltage", + translation_key="rms_voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, + suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ElectricalPowerMeasurement.Attributes.RMSVoltage, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ElectricalPowerMeasurementApparentCurrent", + translation_key="apparent_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ElectricalPowerMeasurement.Attributes.ApparentCurrent, + ), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( key="ElectricalPowerMeasurementActiveCurrent", + translation_key="active_current", device_class=SensorDeviceClass.CURRENT, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, @@ -812,6 +903,40 @@ DISCOVERY_SCHEMAS = [ clusters.ElectricalPowerMeasurement.Attributes.ActiveCurrent, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ElectricalPowerMeasurementReactiveCurrent", + translation_key="reactive_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ElectricalPowerMeasurement.Attributes.ReactiveCurrent, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ElectricalPowerMeasurementRMSCurrent", + translation_key="rms_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + suggested_display_precision=2, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ElectricalPowerMeasurement.Attributes.RMSCurrent, + ), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( @@ -1245,4 +1370,19 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterSensor, required_attributes=(clusters.PumpConfigurationAndControl.Attributes.Speed,), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ValveConfigurationAndControlAutoCloseTime", + translation_key="auto_close_time", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=None, + device_to_ha=(lambda x: dt_util.utc_from_timestamp(x) if x > 0 else None), + ), + entity_class=MatterSensor, + featuremap_contains=clusters.ValveConfigurationAndControl.Bitmaps.Feature.kTimeSync, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.AutoCloseTime, + ), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 749cf387a40..9a0bb77adfa 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -92,8 +92,17 @@ "dishwasher_alarm_inflow": { "name": "Inflow alarm" }, - "dishwasher_alarm_door": { + "alarm_door": { "name": "Door alarm" + }, + "valve_fault_blocked": { + "name": "Valve blocked" + }, + "valve_fault_general_fault": { + "name": "General fault" + }, + "valve_fault_leaking": { + "name": "Valve leaking" } }, "button": { @@ -111,6 +120,9 @@ }, "reset_filter_condition": { "name": "Reset filter condition" + }, + "self_test_request": { + "name": "Self-test" } }, "climate": { @@ -203,6 +215,9 @@ }, "led_indicator_intensity_on": { "name": "LED on intensity" + }, + "valve_configuration_and_control_default_open_duration": { + "name": "Default open duration" } }, "light": { @@ -289,6 +304,9 @@ "activated_carbon_filter_condition": { "name": "Activated carbon filter condition" }, + "auto_close_time": { + "name": "Auto-close time" + }, "contamination_state": { "name": "Contamination state", "state": { @@ -431,6 +449,15 @@ "pump_speed": { "name": "Rotation speed" }, + "eve_weather_trend": { + "name": "Weather trend", + "state": { + "cloudy": "Cloudy", + "rainy": "Rainy", + "sunny": "Sunny", + "stormy": "Stormy" + } + }, "evse_circuit_capacity": { "name": "Circuit capacity" }, @@ -448,6 +475,24 @@ }, "window_covering_target_position": { "name": "Target opening position" + }, + "active_current": { + "name": "Active current" + }, + "apparent_current": { + "name": "Apparent current" + }, + "reactive_current": { + "name": "Reactive current" + }, + "rms_current": { + "name": "Effective current" + }, + "rms_voltage": { + "name": "Effective voltage" + }, + "voltage": { + "name": "Voltage" } }, "switch": { diff --git a/homeassistant/components/matter/valve.py b/homeassistant/components/matter/valve.py index bea11468c6b..715cdc2a09e 100644 --- a/homeassistant/components/matter/valve.py +++ b/homeassistant/components/matter/valve.py @@ -21,7 +21,6 @@ from .helpers import get_matter from .models import MatterDiscoverySchema ValveConfigurationAndControl = clusters.ValveConfigurationAndControl - ValveStateEnum = ValveConfigurationAndControl.Enums.ValveStateEnum @@ -52,9 +51,12 @@ class MatterValve(MatterEntity, ValveEntity): async def async_set_valve_position(self, position: int) -> None: """Move the valve to a specific position.""" - await self.send_device_command( - ValveConfigurationAndControl.Commands.Open(targetLevel=position) - ) + if position > 0: + await self.send_device_command( + ValveConfigurationAndControl.Commands.Open(targetLevel=position) + ) + return + await self.send_device_command(ValveConfigurationAndControl.Commands.Close()) @callback def _update_from_device(self) -> None: diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index db622d21f1a..477e77022de 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.07.21"], + "requirements": ["yt-dlp[default]==2025.08.11"], "single_config_entry": true } diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index be365694579..9b714fdf52d 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -1,5 +1,6 @@ """Intents for the media_player integration.""" +import asyncio from collections.abc import Iterable from dataclasses import dataclass, field import logging @@ -14,21 +15,21 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_SET, + STATE_PLAYING, ) from homeassistant.core import Context, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.entity_component import EntityComponent -from . import ( +from . import MediaPlayerDeviceClass, MediaPlayerEntity +from .browse_media import SearchMedia +from .const import ( + ATTR_MEDIA_FILTER_CLASSES, ATTR_MEDIA_VOLUME_LEVEL, DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SEARCH_MEDIA, - MediaPlayerDeviceClass, - SearchMedia, -) -from .const import ( - ATTR_MEDIA_FILTER_CLASSES, MediaClass, MediaPlayerEntityFeature, MediaPlayerState, @@ -39,6 +40,7 @@ INTENT_MEDIA_UNPAUSE = "HassMediaUnpause" INTENT_MEDIA_NEXT = "HassMediaNext" INTENT_MEDIA_PREVIOUS = "HassMediaPrevious" INTENT_SET_VOLUME = "HassSetVolume" +INTENT_SET_VOLUME_RELATIVE = "HassSetVolumeRelative" INTENT_MEDIA_SEARCH_AND_PLAY = "HassMediaSearchAndPlay" _LOGGER = logging.getLogger(__name__) @@ -127,6 +129,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: device_classes={MediaPlayerDeviceClass}, ), ) + intent.async_register(hass, MediaSetVolumeRelativeHandler()) intent.async_register(hass, MediaSearchAndPlayHandler()) @@ -354,3 +357,120 @@ class MediaSearchAndPlayHandler(intent.IntentHandler): response.async_set_speech_slots({"media": first_result.as_dict()}) response.response_type = intent.IntentResponseType.ACTION_DONE return response + + +class MediaSetVolumeRelativeHandler(intent.IntentHandler): + """Handler for setting relative volume.""" + + description = "Increases or decreases the volume of a media player" + + intent_type = INTENT_SET_VOLUME_RELATIVE + slot_schema = { + vol.Required("volume_step"): vol.Any( + "up", + "down", + vol.All( + vol.Coerce(int), + vol.Range(min=-100, max=100), + lambda val: val / 100, + ), + ), + # Optional name/area/floor slots handled by intent matcher + vol.Optional("name"): cv.string, + vol.Optional("area"): cv.string, + vol.Optional("floor"): cv.string, + vol.Optional("preferred_area_id"): cv.string, + vol.Optional("preferred_floor_id"): cv.string, + } + platforms = {DOMAIN} + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Handle the intent.""" + hass = intent_obj.hass + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] + + slots = self.async_validate_slots(intent_obj.slots) + volume_step = slots["volume_step"]["value"] + + # Entity name to match + name_slot = slots.get("name", {}) + entity_name: str | None = name_slot.get("value") + + # Get area/floor info + area_slot = slots.get("area", {}) + area_id = area_slot.get("value") + + floor_slot = slots.get("floor", {}) + floor_id = floor_slot.get("value") + + # Find matching entities + match_constraints = intent.MatchTargetsConstraints( + name=entity_name, + area_name=area_id, + floor_name=floor_id, + domains={DOMAIN}, + assistant=intent_obj.assistant, + features=MediaPlayerEntityFeature.VOLUME_SET, + ) + match_preferences = intent.MatchTargetsPreferences( + area_id=slots.get("preferred_area_id", {}).get("value"), + floor_id=slots.get("preferred_floor_id", {}).get("value"), + ) + match_result = intent.async_match_targets( + hass, match_constraints, match_preferences + ) + + if not match_result.is_match: + # No targets + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) + + if ( + match_result.is_match + and (len(match_result.states) > 1) + and ("name" not in intent_obj.slots) + ): + # Multiple targets not by name, so we need to check state + match_result.states = [ + s for s in match_result.states if s.state == STATE_PLAYING + ] + if not match_result.states: + # No media players are playing + raise intent.MatchFailedError( + result=intent.MatchTargetsResult( + is_match=False, no_match_reason=intent.MatchFailedReason.STATE + ), + constraints=match_constraints, + preferences=match_preferences, + ) + + target_entity_ids = {s.entity_id for s in match_result.states} + target_entities = [ + e for e in component.entities if e.entity_id in target_entity_ids + ] + + if volume_step == "up": + coros = [e.async_volume_up() for e in target_entities] + elif volume_step == "down": + coros = [e.async_volume_down() for e in target_entities] + else: + coros = [ + e.async_set_volume_level( + max(0.0, min(1.0, e.volume_level + volume_step)) + ) + for e in target_entities + ] + + try: + await asyncio.gather(*coros) + except HomeAssistantError as err: + _LOGGER.error("Error setting relative volume: %s", err) + raise intent.IntentHandleError( + f"Error setting relative volume: {err}" + ) from err + + response = intent_obj.create_response() + response.response_type = intent.IntentResponseType.ACTION_DONE + response.async_set_states(match_result.states) + return response diff --git a/homeassistant/components/mercury_nz/__init__.py b/homeassistant/components/mercury_nz/__init__.py deleted file mode 100644 index ff22fc5ce4a..00000000000 --- a/homeassistant/components/mercury_nz/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Virtual integration: Mercury NZ Limited.""" diff --git a/homeassistant/components/mercury_nz/manifest.json b/homeassistant/components/mercury_nz/manifest.json deleted file mode 100644 index d9d30787067..00000000000 --- a/homeassistant/components/mercury_nz/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "domain": "mercury_nz", - "name": "Mercury NZ Limited", - "integration_type": "virtual", - "supported_by": "opower" -} diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 3a4c3dda507..1d2bf456c9d 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -10,9 +10,12 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, + ATTR_FORECAST_CLOUD_COVERAGE, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_WIND_BEARING, @@ -34,6 +37,9 @@ FORECAST_MAP = { ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_WIND_BEARING: "wind_bearing", ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "wind_gust", + ATTR_FORECAST_CLOUD_COVERAGE: "cloudiness", + ATTR_FORECAST_HUMIDITY: "humidity", } CONDITION_MAP = { diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 68f46f0a656..b6095c174f2 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -138,6 +138,16 @@ class MetEireannWeather( """Return the wind direction.""" return self.coordinator.data.current_weather_data.get("wind_bearing") + @property + def native_wind_gust_speed(self) -> float | None: + """Return the wind gust speed in native units.""" + return self.coordinator.data.current_weather_data.get("wind_gust") + + @property + def cloud_coverage(self) -> float | None: + """Return the cloud coverage.""" + return self.coordinator.data.current_weather_data.get("cloudiness") + def _forecast(self, hourly: bool) -> list[Forecast]: """Return the forecast array.""" if hourly: diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index 2c5c250aee7..173865195df 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from aiohttp import ClientError, ClientResponseError +from pymiele import MieleAPI from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -66,7 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> boo ) from err # Setup MieleAPI and coordinator for data fetch - coordinator = MieleDataUpdateCoordinator(hass, entry, auth) + api = MieleAPI(auth) + coordinator = MieleDataUpdateCoordinator(hass, entry, api) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index e8b626af785..fb5e04fbff0 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -850,6 +850,14 @@ COFFEE_SYSTEM_PROGRAM_ID: dict[int, str] = { 24813: "appliance_settings", # modify profile name } +COFFEE_SYSTEM_PROFILE: dict[range, str] = { + range(24000, 24032): "profile_1", + range(24032, 24064): "profile_2", + range(24064, 24096): "profile_3", + range(24096, 24128): "profile_4", + range(24128, 24160): "profile_5", +} + STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = { 8: "steam_cooking", 19: "microwave", @@ -1330,4 +1338,5 @@ class PlatePowerStep(MieleEnum): plate_step_17 = 17 plate_step_18 = 18 plate_step_boost = 117, 118, 218 + plate_step_boost_2 = 217 missing2none = -9999 diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index d5de2d79cb9..98f5c9f8b1c 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -8,13 +8,12 @@ from dataclasses import dataclass from datetime import timedelta import logging -from pymiele import MieleAction, MieleDevice +from pymiele import MieleAction, MieleAPI, MieleDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .api import AsyncConfigEntryAuth from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -43,7 +42,7 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): self, hass: HomeAssistant, config_entry: MieleConfigEntry, - api: AsyncConfigEntryAuth, + api: MieleAPI, ) -> None: """Initialize the Miele data coordinator.""" super().__init__( diff --git a/homeassistant/components/miele/entity.py b/homeassistant/components/miele/entity.py index 4c6e61f6ea5..57c10f6f7bd 100644 --- a/homeassistant/components/miele/entity.py +++ b/homeassistant/components/miele/entity.py @@ -1,12 +1,11 @@ """Entity base class for the Miele integration.""" -from pymiele import MieleAction, MieleDevice +from pymiele import MieleAction, MieleAPI, MieleDevice from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .api import AsyncConfigEntryAuth from .const import DEVICE_TYPE_TAGS, DOMAIN, MANUFACTURER, MieleAppliance, StateStatus from .coordinator import MieleDataUpdateCoordinator @@ -57,7 +56,7 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]): return self.coordinator.data.actions[self._device_id] @property - def api(self) -> AsyncConfigEntryAuth: + def api(self) -> MieleAPI: """Return the api object.""" return self.coordinator.api diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index 77d94c49ffa..a5dbeb4ec2d 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -76,7 +76,8 @@ "plate_step_16": "mdi:circle-slice-7", "plate_step_17": "mdi:circle-slice-8", "plate_step_18": "mdi:circle-slice-8", - "plate_step_boost": "mdi:alpha-b-circle-outline" + "plate_step_boost": "mdi:alpha-b-circle-outline", + "plate_step_boost_2": "mdi:alpha-b-circle" } }, "program_type": { diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index c9a20e977f9..63ace343dc8 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "bronze", - "requirements": ["pymiele==0.5.2"], + "requirements": ["pymiele==0.5.4"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index cc108841aae..988f25accdc 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -2,14 +2,15 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass import logging -from typing import Final, cast +from typing import Any, Final, cast from pymiele import MieleDevice, MieleTemperature from homeassistant.components.sensor import ( + RestoreSensor, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -18,18 +19,20 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( PERCENTAGE, REVOLUTIONS_PER_MINUTE, + STATE_UNKNOWN, EntityCategory, UnitOfEnergy, UnitOfTemperature, UnitOfTime, UnitOfVolume, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from .const import ( + COFFEE_SYSTEM_PROFILE, DISABLED_TEMP_ENTITIES, DOMAIN, STATE_PROGRAM_ID, @@ -61,6 +64,8 @@ PLATE_COUNT = { "KMX": 6, } +ATTRIBUTE_PROFILE = "profile" + def _get_plate_count(tech_type: str) -> int: """Get number of zones for hob.""" @@ -88,11 +93,22 @@ def _convert_temperature( return raw_value +def _get_coffee_profile(value: MieleDevice) -> str | None: + """Get coffee profile from value.""" + if value.state_program_id is not None: + for key_range, profile in COFFEE_SYSTEM_PROFILE.items(): + if value.state_program_id in key_range: + return profile + return None + + @dataclass(frozen=True, kw_only=True) class MieleSensorDescription(SensorEntityDescription): """Class describing Miele sensor entities.""" value_fn: Callable[[MieleDevice], StateType] + end_value_fn: Callable[[StateType], StateType] | None = None + extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None zone: int | None = None unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None @@ -157,7 +173,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( MieleAppliance.OVEN_MICROWAVE, MieleAppliance.STEAM_OVEN, MieleAppliance.MICROWAVE, - MieleAppliance.COFFEE_SYSTEM, MieleAppliance.ROBOT_VACUUM_CLEANER, MieleAppliance.WASHER_DRYER, MieleAppliance.STEAM_OVEN_COMBI, @@ -172,6 +187,18 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( value_fn=lambda value: value.state_program_id, ), ), + MieleSensorDefinition( + types=(MieleAppliance.COFFEE_SYSTEM,), + description=MieleSensorDescription( + key="state_program_id", + translation_key="program_id", + device_class=SensorDeviceClass.ENUM, + value_fn=lambda value: value.state_program_id, + extra_attributes={ + ATTRIBUTE_PROFILE: _get_coffee_profile, + }, + ), + ), MieleSensorDefinition( types=( MieleAppliance.WASHING_MACHINE, @@ -362,6 +389,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( key="state_elapsed_time", translation_key="elapsed_time", value_fn=lambda value: _convert_duration(value.state_elapsed_time), + end_value_fn=lambda last_value: last_value, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, entity_category=EntityCategory.DIAGNOSTIC, @@ -585,6 +613,7 @@ async def async_setup_entry( "state_program_id": MieleProgramIdSensor, "state_program_phase": MielePhaseSensor, "state_plate_step": MielePlateSensor, + "state_elapsed_time": MieleTimeSensor, }.get(definition.description.key, MieleSensor) def _is_entity_registered(unique_id: str) -> bool: @@ -710,6 +739,46 @@ class MieleSensor(MieleEntity, SensorEntity): """Return the state of the sensor.""" return self.entity_description.value_fn(self.device) + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return extra_state_attributes.""" + if self.entity_description.extra_attributes is None: + return None + attr = {} + for key, value in self.entity_description.extra_attributes.items(): + attr[key] = value(self.device) + return attr + + +class MieleRestorableSensor(MieleSensor, RestoreSensor): + """Representation of a Sensor whose internal state can be restored.""" + + _last_value: StateType + + def __init__( + self, + coordinator: MieleDataUpdateCoordinator, + device_id: str, + description: MieleSensorDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, device_id, description) + self._last_value = None + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + + # recover last value from cache + last_value = await self.async_get_last_state() + if last_value and last_value.state != STATE_UNKNOWN: + self._last_value = last_value.state + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self._last_value + class MielePlateSensor(MieleSensor): """Representation of a Sensor.""" @@ -792,6 +861,8 @@ class MielePhaseSensor(MieleSensor): class MieleProgramIdSensor(MieleSensor): """Representation of the program id sensor.""" + _unrecorded_attributes = frozenset({ATTRIBUTE_PROFILE}) + @property def native_value(self) -> StateType: """Return the state of the sensor.""" @@ -810,3 +881,35 @@ class MieleProgramIdSensor(MieleSensor): def options(self) -> list[str]: """Return the options list for the actual device type.""" return sorted(set(STATE_PROGRAM_ID.get(self.device.device_type, {}).values())) + + +class MieleTimeSensor(MieleRestorableSensor): + """Representation of time sensors keeping state from cache.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + current_value = self.entity_description.value_fn(self.device) + current_status = StateStatus(self.device.state_status) + + # report end-specific value when program ends (some devices are immediately reporting 0...) + if ( + current_status == StateStatus.PROGRAM_ENDED + and self.entity_description.end_value_fn is not None + ): + self._last_value = self.entity_description.end_value_fn(self._last_value) + + # keep value when program ends if no function is specified + elif current_status == StateStatus.PROGRAM_ENDED: + pass + + # force unknown when appliance is not working (some devices are keeping last value until a new cycle starts) + elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE): + self._last_value = None + + # otherwise, cache value and return it + else: + self._last_value = current_value + + super()._handle_coordinator_update() diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 90689a3d9cc..4f0fa48e724 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -223,7 +223,8 @@ "plate_step_16": "8\u2022", "plate_step_17": "9", "plate_step_18": "9\u2022", - "plate_step_boost": "Boost" + "plate_step_boost": "Boost", + "plate_step_boost_2": "Boost 2" } }, "drying_step": { @@ -990,6 +991,18 @@ "yom_tov": "Yom tov", "yorkshire_pudding": "Yorkshire pudding", "zander_fillet": "Zander (fillet)" + }, + "state_attributes": { + "profile": { + "name": "Profile", + "state": { + "profile_1": "Profile 1", + "profile_2": "Profile 2", + "profile_3": "Profile 3", + "profile_4": "Profile 4", + "profile_5": "Profile 5" + } + } } }, "spin_speed": { diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 28d1be24587..a7e2cd51a65 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging from typing import Any from homeassistant.components.binary_sensor import BinarySensorEntity @@ -24,6 +23,7 @@ from homeassistant.helpers.update_coordinator import ( from . import get_hub from .const import ( + _LOGGER, CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CONF_SLAVE_COUNT, @@ -32,8 +32,6 @@ from .const import ( from .entity import BasePlatform from .modbus import ModbusHub -_LOGGER = logging.getLogger(__name__) - PARALLEL_UPDATES = 1 diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index be10a9495c6..f8e7dca245a 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging import struct from typing import Any, cast @@ -44,6 +43,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import get_hub from .const import ( + _LOGGER, CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_COIL, @@ -104,8 +104,6 @@ from .const import ( from .entity import BaseStructPlatform from .modbus import ModbusHub -_LOGGER = logging.getLogger(__name__) - PARALLEL_UPDATES = 1 HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY = { @@ -469,9 +467,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): async def _async_update(self) -> None: """Update Target & Current Temperature.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval - self._attr_target_temperature = await self._async_read_register( CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register[ @@ -495,6 +490,11 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): if hvac_mode == value: self._attr_hvac_mode = mode break + else: + # since there are no hvac_mode_register, this + # integration should not touch the attr. + # However it lacks in the climate component. + self._attr_hvac_mode = HVACMode.AUTO # Read the HVAC action register if defined if self._hvac_action_register is not None: diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 068a46b1f81..dafc604e781 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -1,6 +1,7 @@ """Constants used in modbus integration.""" from enum import Enum +import logging from homeassistant.const import ( CONF_ADDRESS, @@ -96,6 +97,7 @@ CONF_VIRTUAL_COUNT = "virtual_count" CONF_WRITE_TYPE = "write_type" CONF_ZERO_SUPPRESS = "zero_suppress" +DEVICE_ID = "device_id" RTUOVERTCP = "rtuovertcp" SERIAL = "serial" TCP = "tcp" @@ -177,3 +179,5 @@ LIGHT_MAX_BRIGHTNESS = 255 LIGHT_MODBUS_SCALE_MIN = 0 LIGHT_MODBUS_SCALE_MAX = 100 LIGHT_MODBUS_INVALID_VALUE = 0xFFFF + +_LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 5e7b008ff7c..23a09431072 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -123,8 +123,6 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): async def _async_update(self) -> None: """Update the state of the cover.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval result = await self._hub.async_pb_call( self._slave, self._address, 1, self._input_type ) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 53c3e8f8709..689d882a2f3 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -3,10 +3,8 @@ from __future__ import annotations from abc import abstractmethod -import asyncio from collections.abc import Callable from datetime import datetime, timedelta -import logging import struct from typing import Any, cast @@ -29,10 +27,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, ToggleEntity -from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from .const import ( + _LOGGER, CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, @@ -68,8 +67,6 @@ from .const import ( ) from .modbus import ModbusHub -_LOGGER = logging.getLogger(__name__) - class BasePlatform(Entity): """Base for readonly platforms.""" @@ -94,7 +91,6 @@ class BasePlatform(Entity): self._scan_interval = int(entry[CONF_SCAN_INTERVAL]) self._cancel_timer: Callable[[], None] | None = None self._cancel_call: Callable[[], None] | None = None - self._attr_unique_id = entry.get(CONF_UNIQUE_ID) self._attr_name = entry[CONF_NAME] self._attr_device_class = entry.get(CONF_DEVICE_CLASS) @@ -111,29 +107,39 @@ class BasePlatform(Entity): self._max_value = get_optional_numeric_config(CONF_MAX_VALUE) self._nan_value = entry.get(CONF_NAN_VALUE) self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS) - self._update_lock = asyncio.Lock() @abstractmethod async def _async_update(self) -> None: """Virtual function to be overwritten.""" - async def async_update(self, now: datetime | None = None) -> None: + async def async_update(self) -> None: """Update the entity state.""" - async with self._update_lock: - await self._async_update() + if self._cancel_call: + self._cancel_call() + await self.async_local_update() + + async def async_local_update(self, now: datetime | None = None) -> None: + """Update the entity state.""" + await self._async_update() + self.async_write_ha_state() + if self._scan_interval > 0: + self._cancel_call = async_call_later( + self.hass, + timedelta(seconds=self._scan_interval), + self.async_local_update, + ) async def _async_update_write_state(self) -> None: """Update the entity state and write it to the state machine.""" - await self.async_update() - self.async_write_ha_state() + if self._cancel_call: + self._cancel_call() + self._cancel_call = None + await self.async_local_update() async def _async_update_if_not_in_progress( self, now: datetime | None = None ) -> None: """Update the entity state if not already in progress.""" - if self._update_lock.locked(): - _LOGGER.debug("Update for entity %s is already in progress", self.name) - return await self._async_update_write_state() @callback @@ -141,12 +147,9 @@ class BasePlatform(Entity): """Remote start entity.""" self._async_cancel_update_polling() self._async_schedule_future_update(0.1) - if self._scan_interval > 0: - self._cancel_timer = async_track_time_interval( - self.hass, - self._async_update_if_not_in_progress, - timedelta(seconds=self._scan_interval), - ) + self._cancel_call = async_call_later( + self.hass, timedelta(seconds=0.1), self.async_local_update + ) self._attr_available = True self.async_write_ha_state() @@ -179,9 +182,20 @@ class BasePlatform(Entity): self._attr_available = False self.async_write_ha_state() + async def async_await_connection(self, _now: Any) -> None: + """Wait for first connect.""" + await self._hub.event_connected.wait() + self.async_run() + async def async_base_added_to_hass(self) -> None: """Handle entity which will be added.""" - self.async_run() + self.async_on_remove( + async_call_later( + self.hass, + self._hub.config_delay + 0.1, + self.async_await_connection, + ) + ) self.async_on_remove( async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_hold) ) @@ -382,8 +396,6 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): async def _async_update(self) -> None: """Update the entity state.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval if not self._verify_active: self._attr_available = True return diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index c025eefe0e4..7b1035c702b 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging from typing import Any from homeassistant.components.light import ( @@ -35,7 +34,6 @@ from .entity import BaseSwitch from .modbus import ModbusHub PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 656b69920a0..32a043c4379 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/modbus", "iot_class": "local_polling", "loggers": ["pymodbus"], - "requirements": ["pymodbus==3.11.0"] + "requirements": ["pymodbus==3.11.1"] } diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 1304e679347..f8604efdc2f 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -4,8 +4,6 @@ from __future__ import annotations import asyncio from collections import namedtuple -from collections.abc import Callable -import logging from typing import Any from pymodbus.client import ( @@ -29,15 +27,15 @@ from homeassistant.const import ( CONF_TYPE, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from .const import ( + _LOGGER, ATTR_ADDRESS, ATTR_HUB, ATTR_SLAVE, @@ -57,6 +55,7 @@ from .const import ( CONF_PARITY, CONF_STOPBITS, DEFAULT_HUB, + DEVICE_ID, MODBUS_DOMAIN as DOMAIN, PLATFORMS, RTUOVERTCP, @@ -70,9 +69,9 @@ from .const import ( ) from .validators import check_config -_LOGGER = logging.getLogger(__name__) DATA_MODBUS_HUBS: HassKey[dict[str, ModbusHub]] = HassKey(DOMAIN) +PRIMARY_RECONNECT_DELAY = 60 ConfEntry = namedtuple("ConfEntry", "call_type attr func_name value_attr_name") # noqa: PYI024 RunEntry = namedtuple("RunEntry", "attr func value_attr_name") # noqa: PYI024 @@ -254,14 +253,15 @@ class ModbusHub: self._client: ( AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None ) = None - self._async_cancel_listener: Callable[[], None] | None = None - self._in_error = False self._lock = asyncio.Lock() + self.event_connected = asyncio.Event() self.hass = hass self.name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] - self._config_delay = client_config[CONF_DELAY] + self.config_delay = client_config[CONF_DELAY] self._pb_request: dict[str, RunEntry] = {} + self._connect_task: asyncio.Task + self._last_log_error: str = "" self._pb_class = { SERIAL: AsyncModbusSerialClient, TCP: AsyncModbusTcpClient, @@ -302,32 +302,41 @@ class ModbusHub: else: self._msg_wait = 0 - def _log_error(self, text: str, error_state: bool = True) -> None: + def _log_error(self, text: str) -> None: + if text == self._last_log_error: + return + self._last_log_error = text log_text = f"Pymodbus: {self.name}: {text}" - if self._in_error: - _LOGGER.debug(log_text) - else: - _LOGGER.error(log_text) - self._in_error = error_state + _LOGGER.error(log_text) async def async_pb_connect(self) -> None: """Connect to device, async.""" - async with self._lock: - try: - await self._client.connect() # type: ignore[union-attr] - except ModbusException as exception_error: - err = f"{self.name} connect failed, retry in pymodbus ({exception_error!s})" - self._log_error(err, error_state=False) - return - message = f"modbus {self.name} communication open" - _LOGGER.info(message) + while True: + async with self._lock: + try: + if await self._client.connect(): # type: ignore[union-attr] + _LOGGER.info(f"modbus {self.name} communication open") + break + except ModbusException as exception_error: + self._log_error( + f"{self.name} connect failed, please check your configuration ({exception_error!s})" + ) + _LOGGER.info( + f"modbus {self.name} connect NOT a success ! retrying in {PRIMARY_RECONNECT_DELAY} seconds" + ) + await asyncio.sleep(PRIMARY_RECONNECT_DELAY) + + if self.config_delay: + await asyncio.sleep(self.config_delay) + self.config_delay = 0 + self.event_connected.set() async def async_setup(self) -> bool: """Set up pymodbus client.""" try: self._client = self._pb_class[self._config_type](**self._pb_params) except ModbusException as exception_error: - self._log_error(str(exception_error), error_state=False) + self._log_error(str(exception_error)) return False for entry in PB_CALL: @@ -336,23 +345,11 @@ class ModbusHub: entry.attr, func, entry.value_attr_name ) - self.hass.async_create_background_task( + self._connect_task = self.hass.async_create_background_task( self.async_pb_connect(), "modbus-connect" ) - - # Start counting down to allow modbus requests. - if self._config_delay: - self._async_cancel_listener = async_call_later( - self.hass, self._config_delay, self.async_end_delay - ) return True - @callback - def async_end_delay(self, args: Any) -> None: - """End startup delay.""" - self._async_cancel_listener = None - self._config_delay = 0 - async def async_restart(self) -> None: """Reconnect client.""" if self._client: @@ -362,9 +359,9 @@ class ModbusHub: async def async_close(self) -> None: """Disconnect client.""" - if self._async_cancel_listener: - self._async_cancel_listener() - self._async_cancel_listener = None + if not self._connect_task.done(): + self._connect_task.cancel() + async with self._lock: if self._client: try: @@ -381,7 +378,7 @@ class ModbusHub: ) -> ModbusPDU | None: """Call sync. pymodbus.""" kwargs: dict[str, Any] = ( - {ATTR_SLAVE: slave} if slave is not None else {ATTR_SLAVE: 1} + {DEVICE_ID: slave} if slave is not None else {DEVICE_ID: 1} ) entry = self._pb_request[use_call] @@ -410,7 +407,6 @@ class ModbusHub: error = f"Error: device: {slave} address: {address} -> pymodbus returned isError True" self._log_error(error) return None - self._in_error = False return result async def async_pb_call( @@ -421,8 +417,6 @@ class ModbusHub: use_call: str, ) -> ModbusPDU | None: """Convert async to sync pymodbus call.""" - if self._config_delay: - return None async with self._lock: if not self._client: return None diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 490aece587c..b78fda022ed 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging from typing import Any from homeassistant.components.sensor import ( @@ -26,12 +25,10 @@ from homeassistant.helpers.update_coordinator import ( ) from . import get_hub -from .const import CONF_SLAVE_COUNT, CONF_VIRTUAL_COUNT +from .const import _LOGGER, CONF_SLAVE_COUNT, CONF_VIRTUAL_COUNT from .entity import BaseStructPlatform from .modbus import ModbusHub -_LOGGER = logging.getLogger(__name__) - PARALLEL_UPDATES = 1 @@ -109,9 +106,6 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): async def _async_update(self) -> None: """Update the state of the sensor.""" - # remark "now" is a dummy parameter to avoid problems with - # async_track_time_interval - self._cancel_call = None raw_result = await self._hub.async_pb_call( self._slave, self._address, self._count, self._input_type ) diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json index 0749ba4a2c8..dd71785740b 100644 --- a/homeassistant/components/modbus/strings.json +++ b/homeassistant/components/modbus/strings.json @@ -70,14 +70,6 @@ } }, "issues": { - "removed_lazy_error_count": { - "title": "{config_key} configuration key is being removed", - "description": "Please remove the `{config_key}` key from the {integration} entry in your configuration.yaml file and restart Home Assistant to fix this issue. All errors will be reported, as lazy_error_count is accepted but ignored" - }, - "deprecated_retries": { - "title": "{config_key} configuration key is being removed", - "description": "Please remove the `{config_key}` key from the {integration} entry in your configuration.yaml file and restart Home Assistant to fix this issue.\n\nThe maximum number of retries is now fixed to 3." - }, "missing_modbus_name": { "title": "Modbus entry with host {sub_2} missing name", "description": "Please add `{sub_1}` key to the {integration} entry with host `{sub_2}` in your configuration.yaml file and restart Home Assistant to fix this issue\n\n. `{sub_1}: {sub_3}` have been added." diff --git a/homeassistant/components/modern_forms/entity.py b/homeassistant/components/modern_forms/entity.py index c8419295c1f..0fab00f8f22 100644 --- a/homeassistant/components/modern_forms/entity.py +++ b/homeassistant/components/modern_forms/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -31,6 +31,9 @@ class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator """Return device information about this Modern Forms device.""" return DeviceInfo( identifiers={(DOMAIN, self.coordinator.data.info.mac_address)}, + connections={ + (CONNECTION_NETWORK_MAC, self.coordinator.data.info.mac_address) + }, name=self.coordinator.data.info.device_name, manufacturer="Modern Forms", model=self.coordinator.data.info.fan_type, diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 03f758dbdce..a8a4c2e9538 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -149,6 +149,7 @@ from .const import ( CONF_CERTIFICATE, CONF_CLIENT_CERT, CONF_CLIENT_KEY, + CONF_CODE_FORMAT, CONF_COLOR_MODE_STATE_TOPIC, CONF_COLOR_MODE_VALUE_TEMPLATE, CONF_COLOR_TEMP_COMMAND_TEMPLATE, @@ -217,15 +218,18 @@ from .const import ( CONF_OSCILLATION_VALUE_TEMPLATE, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_CLOSE, + CONF_PAYLOAD_LOCK, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PAYLOAD_OPEN, CONF_PAYLOAD_OSCILLATION_OFF, CONF_PAYLOAD_OSCILLATION_ON, CONF_PAYLOAD_PRESS, + CONF_PAYLOAD_RESET, CONF_PAYLOAD_RESET_PERCENTAGE, CONF_PAYLOAD_RESET_PRESET_MODE, CONF_PAYLOAD_STOP, CONF_PAYLOAD_STOP_TILT, + CONF_PAYLOAD_UNLOCK, CONF_PERCENTAGE_COMMAND_TEMPLATE, CONF_PERCENTAGE_COMMAND_TOPIC, CONF_PERCENTAGE_STATE_TOPIC, @@ -262,12 +266,17 @@ from .const import ( CONF_SPEED_RANGE_MIN, CONF_STATE_CLOSED, CONF_STATE_CLOSING, + CONF_STATE_JAMMED, + CONF_STATE_LOCKED, + CONF_STATE_LOCKING, CONF_STATE_OFF, CONF_STATE_ON, CONF_STATE_OPEN, CONF_STATE_OPENING, CONF_STATE_STOPPED, CONF_STATE_TOPIC, + CONF_STATE_UNLOCKED, + CONF_STATE_UNLOCKING, CONF_STATE_VALUE_TEMPLATE, CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUPPORTED_COLOR_MODES, @@ -328,6 +337,7 @@ from .const import ( DEFAULT_ON_COMMAND_TYPE, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_CLOSE, + DEFAULT_PAYLOAD_LOCK, DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PAYLOAD_OFF, DEFAULT_PAYLOAD_ON, @@ -337,6 +347,7 @@ from .const import ( DEFAULT_PAYLOAD_PRESS, DEFAULT_PAYLOAD_RESET, DEFAULT_PAYLOAD_STOP, + DEFAULT_PAYLOAD_UNLOCK, DEFAULT_PORT, DEFAULT_POSITION_CLOSED, DEFAULT_POSITION_OPEN, @@ -345,7 +356,12 @@ from .const import ( DEFAULT_QOS, DEFAULT_SPEED_RANGE_MAX, DEFAULT_SPEED_RANGE_MIN, + DEFAULT_STATE_JAMMED, + DEFAULT_STATE_LOCKED, + DEFAULT_STATE_LOCKING, DEFAULT_STATE_STOPPED, + DEFAULT_STATE_UNLOCKED, + DEFAULT_STATE_UNLOCKING, DEFAULT_TILT_CLOSED_POSITION, DEFAULT_TILT_MAX, DEFAULT_TILT_MIN, @@ -458,6 +474,7 @@ SUBENTRY_PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH, @@ -1148,6 +1165,7 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { is_schema_default=True, ), }, + Platform.LOCK.value: {}, } PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { Platform.BINARY_SENSOR.value: { @@ -2664,6 +2682,93 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { section="advanced_settings", ), }, + Platform.LOCK.value: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_CODE_FORMAT: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=validate(cv.is_regex), + error="invalid_regular_expression", + ), + CONF_PAYLOAD_LOCK: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_LOCK, + section="lock_payload_settings", + ), + CONF_PAYLOAD_UNLOCK: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_UNLOCK, + section="lock_payload_settings", + ), + CONF_PAYLOAD_OPEN: PlatformField( + selector=TEXT_SELECTOR, + required=False, + section="lock_payload_settings", + ), + CONF_PAYLOAD_RESET: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_RESET, + section="lock_payload_settings", + ), + CONF_STATE_LOCKED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_LOCKED, + section="lock_payload_settings", + ), + CONF_STATE_UNLOCKED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_UNLOCKED, + section="lock_payload_settings", + ), + CONF_STATE_LOCKING: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_LOCKING, + section="lock_payload_settings", + ), + CONF_STATE_UNLOCKING: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_UNLOCKING, + section="lock_payload_settings", + ), + CONF_STATE_JAMMED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_JAMMED, + section="lock_payload_settings", + ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + }, } ENTITY_CONFIG_VALIDATOR: dict[ str, @@ -2675,6 +2780,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ Platform.COVER.value: validate_cover_platform_config, Platform.FAN.value: validate_fan_platform_config, Platform.LIGHT.value: validate_light_platform_config, + Platform.LOCK.value: None, Platform.NOTIFY.value: None, Platform.SENSOR.value: validate_sensor_platform_config, Platform.SWITCH.value: None, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 1dfdb8dac53..2128b55c4b0 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -31,6 +31,7 @@ CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" +CONF_CODE_FORMAT = "code_format" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" CONF_DISCOVERY_PREFIX = "discovery_prefix" @@ -127,6 +128,7 @@ CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" CONF_PAYLOAD_CLOSE = "payload_close" +CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_OPEN = "payload_open" CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" @@ -135,6 +137,7 @@ CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage" CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" CONF_PAYLOAD_STOP = "payload_stop" CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt" +CONF_PAYLOAD_UNLOCK = "payload_unlock" CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" @@ -168,11 +171,16 @@ CONF_SPEED_RANGE_MAX = "speed_range_max" CONF_SPEED_RANGE_MIN = "speed_range_min" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" +CONF_STATE_JAMMED = "state_jammed" +CONF_STATE_LOCKED = "state_locked" +CONF_STATE_LOCKING = "state_locking" CONF_STATE_OFF = "state_off" CONF_STATE_ON = "state_on" CONF_STATE_OPEN = "state_open" CONF_STATE_OPENING = "state_opening" CONF_STATE_STOPPED = "state_stopped" +CONF_STATE_UNLOCKED = "state_unlocked" +CONF_STATE_UNLOCKING = "state_unlocking" CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template" @@ -254,6 +262,7 @@ DEFAULT_ON_COMMAND_TYPE = "last" DEFAULT_QOS = 0 DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_CLOSE = "CLOSE" +DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" @@ -263,6 +272,8 @@ DEFAULT_PAYLOAD_OSCILLATE_ON = "oscillate_on" DEFAULT_PAYLOAD_PRESS = "PRESS" DEFAULT_PAYLOAD_STOP = "STOP" DEFAULT_PAYLOAD_RESET = "None" +DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" + DEFAULT_PORT = 1883 DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 @@ -277,7 +288,14 @@ DEFAULT_POSITION_OPEN = 100 DEFAULT_RETAIN = False DEFAULT_SPEED_RANGE_MAX = 100 DEFAULT_SPEED_RANGE_MIN = 1 +DEFAULT_STATE_LOCKED = "LOCKED" +DEFAULT_STATE_LOCKING = "LOCKING" +DEFAULT_STATE_OPEN = "OPEN" +DEFAULT_STATE_OPENING = "OPENING" DEFAULT_STATE_STOPPED = "stopped" +DEFAULT_STATE_UNLOCKED = "UNLOCKED" +DEFAULT_STATE_UNLOCKING = "UNLOCKING" +DEFAULT_STATE_JAMMED = "JAMMED" DEFAULT_WHITE_SCALE = 255 COVER_PAYLOAD = "cover" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 727e689798e..00771ce521f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -27,12 +27,31 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import subscription from .config import MQTT_RW_SCHEMA from .const import ( + CONF_CODE_FORMAT, CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_LOCK, + CONF_PAYLOAD_OPEN, CONF_PAYLOAD_RESET, + CONF_PAYLOAD_UNLOCK, + CONF_STATE_JAMMED, + CONF_STATE_LOCKED, + CONF_STATE_LOCKING, CONF_STATE_OPEN, CONF_STATE_OPENING, CONF_STATE_TOPIC, + CONF_STATE_UNLOCKED, + CONF_STATE_UNLOCKING, + DEFAULT_PAYLOAD_LOCK, + DEFAULT_PAYLOAD_RESET, + DEFAULT_PAYLOAD_UNLOCK, + DEFAULT_STATE_JAMMED, + DEFAULT_STATE_LOCKED, + DEFAULT_STATE_LOCKING, + DEFAULT_STATE_OPEN, + DEFAULT_STATE_OPENING, + DEFAULT_STATE_UNLOCKED, + DEFAULT_STATE_UNLOCKING, ) from .entity import MqttEntity, async_setup_entity_entry_helper from .models import ( @@ -47,31 +66,7 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 -CONF_CODE_FORMAT = "code_format" - -CONF_PAYLOAD_LOCK = "payload_lock" -CONF_PAYLOAD_UNLOCK = "payload_unlock" -CONF_PAYLOAD_OPEN = "payload_open" - -CONF_STATE_LOCKED = "state_locked" -CONF_STATE_LOCKING = "state_locking" - -CONF_STATE_UNLOCKED = "state_unlocked" -CONF_STATE_UNLOCKING = "state_unlocking" -CONF_STATE_JAMMED = "state_jammed" - DEFAULT_NAME = "MQTT Lock" -DEFAULT_PAYLOAD_LOCK = "LOCK" -DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" -DEFAULT_PAYLOAD_OPEN = "OPEN" -DEFAULT_PAYLOAD_RESET = "None" -DEFAULT_STATE_LOCKED = "LOCKED" -DEFAULT_STATE_LOCKING = "LOCKING" -DEFAULT_STATE_OPEN = "OPEN" -DEFAULT_STATE_OPENING = "OPENING" -DEFAULT_STATE_UNLOCKED = "UNLOCKED" -DEFAULT_STATE_UNLOCKING = "UNLOCKING" -DEFAULT_STATE_JAMMED = "JAMMED" MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( { diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index c3cc31bf04f..9da68e62d80 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -16,6 +16,7 @@ from homeassistant.components.number import ( NumberMode, RestoreNumber, ) +from homeassistant.components.sensor import AMBIGUOUS_UNITS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -70,6 +71,12 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( def validate_config(config: ConfigType) -> ConfigType: """Validate that the configuration is valid, throws if it isn't.""" + if ( + CONF_UNIT_OF_MEASUREMENT in config + and (unit_of_measurement := config[CONF_UNIT_OF_MEASUREMENT]) in AMBIGUOUS_UNITS + ): + config[CONF_UNIT_OF_MEASUREMENT] = AMBIGUOUS_UNITS[unit_of_measurement] + if config[CONF_MIN] > config[CONF_MAX]: raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}") diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 83679894d71..3423fc161ce 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import sensor from homeassistant.components.sensor import ( + AMBIGUOUS_UNITS, CONF_STATE_CLASS, DEVICE_CLASS_UNITS, DEVICE_CLASSES_SCHEMA, @@ -133,9 +134,14 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT f"together with state class '{state_class}'" ) - if (device_class := config.get(CONF_DEVICE_CLASS)) is None or ( - unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) - ) is None: + if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None: + return config + + unit_of_measurement = config[CONF_UNIT_OF_MEASUREMENT] = AMBIGUOUS_UNITS.get( + unit_of_measurement, unit_of_measurement + ) + + if (device_class := config.get(CONF_DEVICE_CLASS)) is None: return config if ( diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 77a476bf40c..3844cf8d669 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -308,6 +308,7 @@ "data": { "blue_template": "Blue template", "brightness_template": "Brightness template", + "code_format": "Code format", "command_template": "Command template", "command_topic": "Command topic", "command_off_template": "Command \"off\" template", @@ -340,6 +341,7 @@ "data_description": { "blue_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract blue color from the state payload value. Expected result of the template is an integer from 0-255 range.", "brightness_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract brightness from the state payload value. Expected result of the template is an integer from 0-255 range.", + "code_format": "A regular expression to validate a supplied code when it is set during the action to open, lock or unlock the MQTT lock. [Learn more.]({url}#code_format)", "command_off_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"off\" state changes. Available variables are: `state` and `transition`.", "command_on_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"on\" state changes. Available variables: `state`, `brightness`, `color_temp`, `red`, `green`, `blue`, `hue`, `sat`, `flash`, `transition` and `effect`. Values `red`, `green`, `blue` and `brightness` are provided as integers from range 0-255. Value of `hue` is provided as float from range 0-360. Value of `sat` is provided as float from range 0-100. Value of `color_temp` is provided as integer representing Kelvin units.", "command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.", @@ -596,6 +598,31 @@ "brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value." } }, + "lock_payload_settings": { + "name": "Lock payload settings", + "data": { + "payload_lock": "Payload \"lock\"", + "payload_open": "Payload \"open\"", + "payload_reset": "Payload \"reset\"", + "payload_unlock": "Payload \"unlock\"", + "state_jammed": "State \"jammed\"", + "state_locked": "State \"locked\"", + "state_locking": "State \"locking\"", + "state_unlocked": "State \"unlocked\"", + "state_unlocking": "State \"unlocking\"" + }, + "data_description": { + "payload_lock": "The payload sent when a \"lock\" command is issued.", + "payload_open": "The payload sent when an \"open\" command is issued. Set this payload if your lock supports the \"open\" action.", + "payload_reset": "The payload received at the state topic that resets the lock to an unknown state.", + "payload_unlock": "The payload sent when an \"unlock\" command is issued.", + "state_jammed": "The payload received at the state topic that represents the \"jammed\" state.", + "state_locked": "The payload received at the state topic that represents the \"locked\" state.", + "state_locking": "The payload received at the state topic that represents the \"locking\" state.", + "state_unlocked": "The payload received at the state topic that represents the \"unlocked\" state.", + "state_unlocking": "The payload received at the state topic that represents the \"unlocking\" state." + } + }, "fan_direction_settings": { "name": "Direction settings", "data": { @@ -911,6 +938,7 @@ "fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min", "fan_preset_mode_reset_in_preset_modes_list": "Payload \"reset preset mode\" is not a valid as a preset mode", "invalid_input": "Invalid value", + "invalid_regular_expression": "Must be a valid regular expression", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_template": "Invalid template", "invalid_supported_color_modes": "Invalid supported color modes selection", @@ -1201,6 +1229,7 @@ "cover": "[%key:component::cover::title%]", "fan": "[%key:component::fan::title%]", "light": "[%key:component::light::title%]", + "lock": "[%key:component::lock::title%]", "notify": "[%key:component::notify::title%]", "sensor": "[%key:component::sensor::title%]", "switch": "[%key:component::switch::title%]" diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index c5a981dbf46..fa033700043 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mystrom", "iot_class": "local_polling", "loggers": ["pymystrom"], - "requirements": ["python-mystrom==2.4.0"] + "requirements": ["python-mystrom==2.5.0"] } diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 253387c254a..d62168a4ad3 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -10,7 +10,12 @@ from typing import Any, Final, cast from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_USER, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.json import save_json @@ -200,7 +205,9 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="unknown") name = self.nanoleaf.name - await self.async_set_unique_id(name) + await self.async_set_unique_id( + name, raise_on_progress=self.source != SOURCE_USER + ) self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host}) if discovery_integration_import: diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 595c57b1b4b..aeb4ffa0c55 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==9.2.1"] + "requirements": ["pyatmo==9.2.3"] } diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index d10a1728a94..4fdbcdb7175 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["nextdns"], - "requirements": ["nextdns==4.0.0"] + "requirements": ["nextdns==4.1.0"] } diff --git a/homeassistant/components/nextdns/strings.json b/homeassistant/components/nextdns/strings.json index 8d7bd6a215f..a9bf635673a 100644 --- a/homeassistant/components/nextdns/strings.json +++ b/homeassistant/components/nextdns/strings.json @@ -328,6 +328,9 @@ "block_zoom": { "name": "Block Zoom" }, + "bypass_age_verification": { + "name": "Bypass age verification" + }, "cache_boost": { "name": "Cache boost" }, diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index 872f7430b3d..48151eb185c 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -56,6 +56,12 @@ SWITCHES = ( entity_category=EntityCategory.CONFIG, state=lambda data: data.anonymized_ecs, ), + NextDnsSwitchEntityDescription( + key="bav", + translation_key="bypass_age_verification", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.bav, + ), NextDnsSwitchEntityDescription( key="logs", translation_key="logs", diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index be37a802d47..cfbdd87a0e2 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -52,6 +52,9 @@ async def async_setup_entry( ) +PARALLEL_UPDATES = 0 + + class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEntity): """Representation of an NINA warning.""" diff --git a/homeassistant/components/nina/diagnostics.py b/homeassistant/components/nina/diagnostics.py new file mode 100644 index 00000000000..f62b7b6bcec --- /dev/null +++ b/homeassistant/components/nina/diagnostics.py @@ -0,0 +1,24 @@ +"""Diagnostics for the Nina integration.""" + +from dataclasses import asdict +from typing import Any + +from homeassistant.core import HomeAssistant + +from .coordinator import NinaConfigEntry + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: NinaConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + runtime_data_dict = { + region_key: [asdict(warning) for warning in region_data] + for region_key, region_data in entry.runtime_data.data.items() + } + + return { + "entry_data": dict(entry.data), + "data": runtime_data_dict, + } diff --git a/homeassistant/components/nobo_hub/select.py b/homeassistant/components/nobo_hub/select.py index c24dbe3d21d..566ff88abac 100644 --- a/homeassistant/components/nobo_hub/select.py +++ b/homeassistant/components/nobo_hub/select.py @@ -69,10 +69,12 @@ class NoboGlobalSelector(SelectEntity): self._override_type = override_type self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, hub.hub_serial)}, + serial_number=hub.hub_serial, name=hub.hub_info[ATTR_NAME], manufacturer=NOBO_MANUFACTURER, - model=f"Nobø Ecohub ({hub.hub_info[ATTR_HARDWARE_VERSION]})", + model="Nobø Ecohub", sw_version=hub.hub_info[ATTR_SOFTWARE_VERSION], + hw_version=hub.hub_info[ATTR_HARDWARE_VERSION], ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index 382fd1b0bf4..6a394f23f4c 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -58,6 +58,7 @@ class NoboTemperatureSensor(SensorEntity): suggested_area = hub.zones[zone_id][ATTR_NAME] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, component[ATTR_SERIAL])}, + serial_number=component[ATTR_SERIAL], name=component[ATTR_NAME], manufacturer=NOBO_MANUFACTURER, model=component[ATTR_MODEL].name, diff --git a/homeassistant/components/notion/entity.py b/homeassistant/components/notion/entity.py index 11e470f1d26..387eaf2e423 100644 --- a/homeassistant/components/notion/entity.py +++ b/homeassistant/components/notion/entity.py @@ -45,9 +45,9 @@ class NotionEntity(CoordinatorEntity[NotionDataUpdateCoordinator]): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, sensor.hardware_id)}, manufacturer="Silicon Labs", - model=str(sensor.hardware_revision), name=str(sensor.name).capitalize(), sw_version=sensor.firmware_version, + hw_version=str(sensor.hardware_revision), ) if bridge := self._async_get_bridge(bridge_id): diff --git a/homeassistant/components/ntfy/manifest.json b/homeassistant/components/ntfy/manifest.json index d9d864d10a3..f041b02b6d6 100644 --- a/homeassistant/components/ntfy/manifest.json +++ b/homeassistant/components/ntfy/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["aionfty"], "quality_scale": "bronze", - "requirements": ["aiontfy==0.5.3"] + "requirements": ["aiontfy==0.5.4"] } diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 79ed56d2a75..1ebd35711ac 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -31,6 +31,7 @@ from homeassistant.loader import async_suggest_report_issue from homeassistant.util.hass_dict import HassKey from .const import ( # noqa: F401 + AMBIGUOUS_UNITS, ATTR_MAX, ATTR_MIN, ATTR_STEP, @@ -368,6 +369,15 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self.entity_description.native_unit_of_measurement return None + @final + @property + def __native_unit_of_measurement_compat(self) -> str | None: + """Process ambiguous units.""" + native_unit_of_measurement = self.native_unit_of_measurement + return AMBIGUOUS_UNITS.get( + native_unit_of_measurement, native_unit_of_measurement + ) + @property @final def unit_of_measurement(self) -> str | None: @@ -375,7 +385,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if self._number_option_unit_of_measurement: return self._number_option_unit_of_measurement - native_unit_of_measurement = self.native_unit_of_measurement + native_unit_of_measurement = self.__native_unit_of_measurement_compat # device_class is checked after native_unit_of_measurement since most # of the time we can avoid the device_class check if ( @@ -444,7 +454,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if device_class not in UNIT_CONVERTERS: return value - native_unit_of_measurement = self.native_unit_of_measurement + native_unit_of_measurement = self.__native_unit_of_measurement_compat unit_of_measurement = self.unit_of_measurement if native_unit_of_measurement != unit_of_measurement: if TYPE_CHECKING: @@ -473,7 +483,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS: return value - native_unit_of_measurement = self.native_unit_of_measurement + native_unit_of_measurement = self.__native_unit_of_measurement_compat unit_of_measurement = self.unit_of_measurement if native_unit_of_measurement != unit_of_measurement: if TYPE_CHECKING: @@ -496,7 +506,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): (number_options := self.registry_entry.options.get(DOMAIN)) and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) and (device_class := self.device_class) in UNIT_CONVERTERS - and self.native_unit_of_measurement + and self.__native_unit_of_measurement_compat in UNIT_CONVERTERS[device_class].VALID_UNITS and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS ): diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index bfb74d621c3..93fbfac2ebb 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.const import ( CONCENTRATION_GRAMS_PER_CUBIC_METER, + CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -88,7 +89,7 @@ class NumberDeviceClass(StrEnum): APPARENT_POWER = "apparent_power" """Apparent power. - Unit of measurement: `VA` + Unit of measurement: `mVA`, `VA` """ AQI = "aqi" @@ -136,7 +137,7 @@ class NumberDeviceClass(StrEnum): CONDUCTIVITY = "conductivity" """Conductivity. - Unit of measurement: `S/cm`, `mS/cm`, `µS/cm` + Unit of measurement: `S/cm`, `mS/cm`, `μS/cm` """ CURRENT = "current" @@ -168,7 +169,7 @@ class NumberDeviceClass(StrEnum): DURATION = "duration" """Fixed duration. - Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs` + Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs` """ ENERGY = "energy" @@ -246,25 +247,25 @@ class NumberDeviceClass(StrEnum): NITROGEN_DIOXIDE = "nitrogen_dioxide" """Amount of NO2. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ NITROGEN_MONOXIDE = "nitrogen_monoxide" """Amount of NO. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ NITROUS_OXIDE = "nitrous_oxide" """Amount of N2O. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ OZONE = "ozone" """Amount of O3. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ PH = "ph" @@ -276,19 +277,19 @@ class NumberDeviceClass(StrEnum): PM1 = "pm1" """Particulate matter <= 1 μm. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ PM10 = "pm10" """Particulate matter <= 10 μm. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ PM25 = "pm25" """Particulate matter <= 2.5 μm. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ POWER_FACTOR = "power_factor" @@ -338,7 +339,7 @@ class NumberDeviceClass(StrEnum): REACTIVE_POWER = "reactive_power" """Reactive power. - Unit of measurement: `var`, `kvar` + Unit of measurement: `mvar`, `var`, `kvar` """ SIGNAL_STRENGTH = "signal_strength" @@ -365,7 +366,7 @@ class NumberDeviceClass(StrEnum): SULPHUR_DIOXIDE = "sulphur_dioxide" """Amount of SO2. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ TEMPERATURE = "temperature" @@ -377,7 +378,7 @@ class NumberDeviceClass(StrEnum): VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. - Unit of measurement: `µg/m³`, `mg/m³` + Unit of measurement: `μg/m³`, `mg/m³` """ VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" @@ -389,7 +390,7 @@ class NumberDeviceClass(StrEnum): VOLTAGE = "voltage" """Voltage. - Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV` + Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV` """ VOLUME = "volume" @@ -417,7 +418,7 @@ class NumberDeviceClass(StrEnum): """Generic flow rate Unit of measurement: UnitOfVolumeFlowRate - - SI / metric: `m³/h`, `L/min`, `mL/s` + - SI / metric: `m³/h`, `m³/min`, `m³/s`, `L/h`, `L/min`, `L/s`, `mL/s` - USCS / imperial: `ft³/min`, `gal/min` """ @@ -436,7 +437,7 @@ class NumberDeviceClass(StrEnum): Weight is used instead of mass to fit with every day language. Unit of measurement: `MASS_*` units - - SI / metric: `µg`, `mg`, `g`, `kg` + - SI / metric: `μg`, `mg`, `g`, `kg` - USCS / imperial: `oz`, `lb` """ @@ -556,3 +557,16 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = { NumberDeviceClass.TEMPERATURE: TemperatureConverter, NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter, } + +# We translate units that were using using the legacy coding of μ \u00b5 +# to units using recommended coding of μ \u03bc +AMBIGUOUS_UNITS: dict[str | None, str] = { + "\u00b5Sv/h": "μSv/h", # aranet: radiation rate + "\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM, + "\u00b5V": UnitOfElectricPotential.MICROVOLT, + "\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + "\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light + "\u00b5g": UnitOfMass.MICROGRAMS, + "\u00b5s": UnitOfTime.MICROSECONDS, +} diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 1ee85a84caf..608f2c2e495 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -7,6 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["aionut"], + "quality_scale": "platinum", "requirements": ["aionut==4.3.4"], "zeroconf": ["_nut._tcp.local."] } diff --git a/homeassistant/components/nut/quality_scale.yaml b/homeassistant/components/nut/quality_scale.yaml new file mode 100644 index 00000000000..823b1091ef6 --- /dev/null +++ b/homeassistant/components/nut/quality_scale.yaml @@ -0,0 +1,90 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + No custom service actions are registered + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: | + No custom service actions are registered + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + No custom event subscriptions are available + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: | + No custom service actions are registered + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: | + No configuration parameters are available + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: done + test-coverage: done + + # Gold + devices: done + diagnostics: done + discovery-update-info: + status: exempt + comment: | + The NUT server has no unique id for reliably determining updates + discovery: done + docs-data-update: done + docs-examples: done + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done + dynamic-devices: + status: exempt + comment: | + Device type integration + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: done + icon-translations: done + reconfiguration-flow: done + repair-issues: + status: exempt + comment: | + No repairable issues are raised + stale-devices: + status: exempt + comment: | + Device type integration + + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: | + Integration uses NUT protocol and does not communicate via HTTP/HTTPS + strict-typing: done diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index da5ccae11a5..42e65bd0db2 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -137,9 +137,11 @@ class OndiloICO(CoordinatorEntity[OndiloIcoMeasuresCoordinator], SensorEntity): super().__init__(coordinator) self.entity_description = description self._pool_id = pool_id - self._attr_unique_id = f"{pool_data.ico['serial_number']}-{description.key}" + serial_number = pool_data.ico["serial_number"] + self._attr_unique_id = f"{serial_number}-{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, pool_data.ico["serial_number"])}, + identifiers={(DOMAIN, serial_number)}, + serial_number=serial_number, ) @property diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 7d6b3e2c019..c1d34bad60e 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -41,6 +41,13 @@ class OneWireBinarySensorEntityDescription( DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ...]] = { + "05": ( + OneWireBinarySensorEntityDescription( + key="sensed", + entity_registry_enabled_default=False, + translation_key="sensed", + ), + ), "12": tuple( OneWireBinarySensorEntityDescription( key=f"sensed.{device_key}", diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 5e7719673b1..c77f2933fe9 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -37,6 +37,9 @@ }, "entity": { "binary_sensor": { + "sensed": { + "name": "Sensed" + }, "sensed_id": { "name": "Sensed {id}" }, diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index fbb1454ec2a..7ebe5256010 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/onvif", "iot_class": "local_push", "loggers": ["onvif", "wsdiscovery", "zeep"], - "requirements": ["onvif-zeep-async==4.0.2", "WSDiscovery==2.1.2"] + "requirements": ["onvif-zeep-async==4.0.4", "WSDiscovery==2.1.2"] } diff --git a/homeassistant/components/open_router/entity.py b/homeassistant/components/open_router/entity.py index ac01ec89704..aa74442f7f4 100644 --- a/homeassistant/components/open_router/entity.py +++ b/homeassistant/components/open_router/entity.py @@ -9,15 +9,15 @@ from typing import TYPE_CHECKING, Any, Literal import openai from openai.types.chat import ( ChatCompletionAssistantMessageParam, + ChatCompletionFunctionToolParam, ChatCompletionMessage, + ChatCompletionMessageFunctionToolCallParam, ChatCompletionMessageParam, - ChatCompletionMessageToolCallParam, ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, - ChatCompletionToolParam, ChatCompletionUserMessageParam, ) -from openai.types.chat.chat_completion_message_tool_call_param import Function +from openai.types.chat.chat_completion_message_function_tool_call_param import Function from openai.types.shared_params import FunctionDefinition, ResponseFormatJSONSchema from openai.types.shared_params.response_format_json_schema import JSONSchema import voluptuous as vol @@ -84,7 +84,7 @@ def _format_structured_output( def _format_tool( tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None, -) -> ChatCompletionToolParam: +) -> ChatCompletionFunctionToolParam: """Format tool specification.""" tool_spec = FunctionDefinition( name=tool.name, @@ -92,7 +92,7 @@ def _format_tool( ) if tool.description: tool_spec["description"] = tool.description - return ChatCompletionToolParam(type="function", function=tool_spec) + return ChatCompletionFunctionToolParam(type="function", function=tool_spec) def _convert_content_to_chat_message( @@ -121,7 +121,7 @@ def _convert_content_to_chat_message( ) if isinstance(content, conversation.AssistantContent) and content.tool_calls: param["tool_calls"] = [ - ChatCompletionMessageToolCallParam( + ChatCompletionMessageFunctionToolCallParam( type="function", id=tool_call.id, function=Function( @@ -160,6 +160,7 @@ async def _transform_response( tool_args=_decode_tool_arguments(tool_call.function.arguments), ) for tool_call in message.tool_calls + if tool_call.type == "function" ] yield data @@ -199,7 +200,7 @@ class OpenRouterEntity(Entity): "extra_body": {"require_parameters": True}, } - tools: list[ChatCompletionToolParam] | None = None + tools: list[ChatCompletionFunctionToolParam] | None = None if chat_log.llm_api: tools = [ _format_tool(tool, chat_log.llm_api.custom_serializer) diff --git a/homeassistant/components/open_router/manifest.json b/homeassistant/components/open_router/manifest.json index 8f989e63189..4a406e06139 100644 --- a/homeassistant/components/open_router/manifest.json +++ b/homeassistant/components/open_router/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["openai==1.93.3", "python-open-router==0.3.1"] + "requirements": ["openai==1.99.5", "python-open-router==0.3.1"] } diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index c45c2b997b3..0b2fa75b5c0 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -49,6 +49,7 @@ from .const import ( CONF_RECOMMENDED, CONF_TEMPERATURE, CONF_TOP_P, + CONF_VERBOSITY, CONF_WEB_SEARCH, CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_CONTEXT_SIZE, @@ -67,6 +68,7 @@ from .const import ( RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TEMPERATURE, RECOMMENDED_TOP_P, + RECOMMENDED_VERBOSITY, RECOMMENDED_WEB_SEARCH, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE, RECOMMENDED_WEB_SEARCH_USER_LOCATION, @@ -323,7 +325,7 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow): model = options[CONF_CHAT_MODEL] - if model.startswith("o"): + if model.startswith(("o", "gpt-5")): step_schema.update( { vol.Optional( @@ -331,7 +333,9 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow): default=RECOMMENDED_REASONING_EFFORT, ): SelectSelector( SelectSelectorConfig( - options=["low", "medium", "high"], + options=["low", "medium", "high"] + if model.startswith("o") + else ["minimal", "low", "medium", "high"], translation_key=CONF_REASONING_EFFORT, mode=SelectSelectorMode.DROPDOWN, ) @@ -341,6 +345,24 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow): elif CONF_REASONING_EFFORT in options: options.pop(CONF_REASONING_EFFORT) + if model.startswith("gpt-5"): + step_schema.update( + { + vol.Optional( + CONF_VERBOSITY, + default=RECOMMENDED_VERBOSITY, + ): SelectSelector( + SelectSelectorConfig( + options=["low", "medium", "high"], + translation_key=CONF_VERBOSITY, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + elif CONF_VERBOSITY in options: + options.pop(CONF_VERBOSITY) + if self._subentry_type == "conversation" and not model.startswith( tuple(UNSUPPORTED_WEB_SEARCH_MODELS) ): diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index cacef6fcff9..2fd18913207 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -21,6 +21,7 @@ CONF_REASONING_EFFORT = "reasoning_effort" CONF_RECOMMENDED = "recommended" CONF_TEMPERATURE = "temperature" CONF_TOP_P = "top_p" +CONF_VERBOSITY = "verbosity" CONF_WEB_SEARCH = "web_search" CONF_WEB_SEARCH_USER_LOCATION = "user_location" CONF_WEB_SEARCH_CONTEXT_SIZE = "search_context_size" @@ -34,6 +35,7 @@ RECOMMENDED_MAX_TOKENS = 3000 RECOMMENDED_REASONING_EFFORT = "low" RECOMMENDED_TEMPERATURE = 1.0 RECOMMENDED_TOP_P = 1.0 +RECOMMENDED_VERBOSITY = "medium" RECOMMENDED_WEB_SEARCH = False RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE = "medium" RECOMMENDED_WEB_SEARCH_USER_LOCATION = False diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index c1b2f970f07..44d833c8e71 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -3,17 +3,18 @@ from __future__ import annotations import base64 -from collections.abc import AsyncGenerator, Callable +from collections.abc import AsyncGenerator, Callable, Iterable import json from mimetypes import guess_file_type from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any, Literal import openai from openai._streaming import AsyncStream from openai.types.responses import ( EasyInputMessageParam, FunctionToolParam, + ResponseCodeInterpreterToolCall, ResponseCompletedEvent, ResponseErrorEvent, ResponseFailedEvent, @@ -21,6 +22,8 @@ from openai.types.responses import ( ResponseFunctionCallArgumentsDoneEvent, ResponseFunctionToolCall, ResponseFunctionToolCallParam, + ResponseFunctionWebSearch, + ResponseFunctionWebSearchParam, ResponseIncompleteEvent, ResponseInputFileParam, ResponseInputImageParam, @@ -29,14 +32,15 @@ from openai.types.responses import ( ResponseOutputItemAddedEvent, ResponseOutputItemDoneEvent, ResponseOutputMessage, - ResponseOutputMessageParam, ResponseReasoningItem, ResponseReasoningItemParam, + ResponseReasoningSummaryTextDeltaEvent, ResponseStreamEvent, ResponseTextDeltaEvent, ToolParam, WebSearchToolParam, ) +from openai.types.responses.response_create_params import ResponseCreateParamsStreaming from openai.types.responses.response_input_param import FunctionCallOutput from openai.types.responses.tool_param import ( CodeInterpreter, @@ -61,6 +65,7 @@ from .const import ( CONF_REASONING_EFFORT, CONF_TEMPERATURE, CONF_TOP_P, + CONF_VERBOSITY, CONF_WEB_SEARCH, CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_CONTEXT_SIZE, @@ -75,6 +80,7 @@ from .const import ( RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TEMPERATURE, RECOMMENDED_TOP_P, + RECOMMENDED_VERBOSITY, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE, ) @@ -89,6 +95,8 @@ MAX_TOOL_ITERATIONS = 10 def _adjust_schema(schema: dict[str, Any]) -> None: """Adjust the schema to be compatible with OpenAI API.""" if schema["type"] == "object": + schema.setdefault("strict", True) + schema.setdefault("additionalProperties", False) if "properties" not in schema: return @@ -122,8 +130,6 @@ def _format_structured_output( _adjust_schema(result) - result["strict"] = True - result["additionalProperties"] = False return result @@ -141,70 +147,198 @@ def _format_tool( def _convert_content_to_param( - content: conversation.Content, + chat_content: Iterable[conversation.Content], ) -> ResponseInputParam: """Convert any native chat message for this agent to the native format.""" messages: ResponseInputParam = [] - if isinstance(content, conversation.ToolResultContent): - return [ - FunctionCallOutput( - type="function_call_output", - call_id=content.tool_call_id, - output=json.dumps(content.tool_result), - ) - ] + reasoning_summary: list[str] = [] + web_search_calls: dict[str, ResponseFunctionWebSearchParam] = {} - if content.content: - role: Literal["user", "assistant", "system", "developer"] = content.role - if role == "system": - role = "developer" - messages.append( - EasyInputMessageParam(type="message", role=role, content=content.content) - ) + for content in chat_content: + if isinstance(content, conversation.ToolResultContent): + if ( + content.tool_name == "web_search_call" + and content.tool_call_id in web_search_calls + ): + web_search_call = web_search_calls.pop(content.tool_call_id) + web_search_call["status"] = content.tool_result.get( # type: ignore[typeddict-item] + "status", "completed" + ) + messages.append(web_search_call) + else: + messages.append( + FunctionCallOutput( + type="function_call_output", + call_id=content.tool_call_id, + output=json.dumps(content.tool_result), + ) + ) + continue - if isinstance(content, conversation.AssistantContent) and content.tool_calls: - messages.extend( - ResponseFunctionToolCallParam( - type="function_call", - name=tool_call.tool_name, - arguments=json.dumps(tool_call.tool_args), - call_id=tool_call.id, + if content.content: + role: Literal["user", "assistant", "system", "developer"] = content.role + if role == "system": + role = "developer" + messages.append( + EasyInputMessageParam( + type="message", role=role, content=content.content + ) ) - for tool_call in content.tool_calls - ) + + if isinstance(content, conversation.AssistantContent): + if content.tool_calls: + for tool_call in content.tool_calls: + if ( + tool_call.external + and tool_call.tool_name == "web_search_call" + and "action" in tool_call.tool_args + ): + web_search_calls[tool_call.id] = ResponseFunctionWebSearchParam( + type="web_search_call", + id=tool_call.id, + action=tool_call.tool_args["action"], + status="completed", + ) + else: + messages.append( + ResponseFunctionToolCallParam( + type="function_call", + name=tool_call.tool_name, + arguments=json.dumps(tool_call.tool_args), + call_id=tool_call.id, + ) + ) + + if content.thinking_content: + reasoning_summary.append(content.thinking_content) + + if isinstance(content.native, ResponseReasoningItem): + messages.append( + ResponseReasoningItemParam( + type="reasoning", + id=content.native.id, + summary=[ + { + "type": "summary_text", + "text": summary, + } + for summary in reasoning_summary + ] + if content.thinking_content + else [], + encrypted_content=content.native.encrypted_content, + ) + ) + reasoning_summary = [] + return messages async def _transform_stream( chat_log: conversation.ChatLog, - result: AsyncStream[ResponseStreamEvent], - messages: ResponseInputParam, -) -> AsyncGenerator[conversation.AssistantContentDeltaDict]: + stream: AsyncStream[ResponseStreamEvent], +) -> AsyncGenerator[ + conversation.AssistantContentDeltaDict | conversation.ToolResultContentDeltaDict +]: """Transform an OpenAI delta stream into HA format.""" - async for event in result: + last_summary_index = None + last_role: Literal["assistant", "tool_result"] | None = None + + async for event in stream: LOGGER.debug("Received event: %s", event) if isinstance(event, ResponseOutputItemAddedEvent): - if isinstance(event.item, ResponseOutputMessage): - yield {"role": event.item.role} - elif isinstance(event.item, ResponseFunctionToolCall): + if isinstance(event.item, ResponseFunctionToolCall): # OpenAI has tool calls as individual events # while HA puts tool calls inside the assistant message. # We turn them into individual assistant content for HA # to ensure that tools are called as soon as possible. yield {"role": "assistant"} + last_role = "assistant" + last_summary_index = None current_tool_call = event.item + elif ( + isinstance(event.item, ResponseOutputMessage) + or ( + isinstance(event.item, ResponseReasoningItem) + and last_summary_index is not None + ) # Subsequent ResponseReasoningItem + or last_role != "assistant" + ): + yield {"role": "assistant"} + last_role = "assistant" + last_summary_index = None elif isinstance(event, ResponseOutputItemDoneEvent): - item = event.item.model_dump() - item.pop("status", None) if isinstance(event.item, ResponseReasoningItem): - messages.append(cast(ResponseReasoningItemParam, item)) - elif isinstance(event.item, ResponseOutputMessage): - messages.append(cast(ResponseOutputMessageParam, item)) - elif isinstance(event.item, ResponseFunctionToolCall): - messages.append(cast(ResponseFunctionToolCallParam, item)) + yield { + "native": ResponseReasoningItem( + type="reasoning", + id=event.item.id, + summary=[], # Remove summaries + encrypted_content=event.item.encrypted_content, + ) + } + last_summary_index = len(event.item.summary) - 1 + elif isinstance(event.item, ResponseCodeInterpreterToolCall): + yield { + "tool_calls": [ + llm.ToolInput( + id=event.item.id, + tool_name="code_interpreter", + tool_args={ + "code": event.item.code, + "container": event.item.container_id, + }, + external=True, + ) + ] + } + yield { + "role": "tool_result", + "tool_call_id": event.item.id, + "tool_name": "code_interpreter", + "tool_result": { + "output": [output.to_dict() for output in event.item.outputs] # type: ignore[misc] + if event.item.outputs is not None + else None + }, + } + last_role = "tool_result" + elif isinstance(event.item, ResponseFunctionWebSearch): + yield { + "tool_calls": [ + llm.ToolInput( + id=event.item.id, + tool_name="web_search_call", + tool_args={ + "action": event.item.action.to_dict(), + }, + external=True, + ) + ] + } + yield { + "role": "tool_result", + "tool_call_id": event.item.id, + "tool_name": "web_search_call", + "tool_result": {"status": event.item.status}, + } + last_role = "tool_result" elif isinstance(event, ResponseTextDeltaEvent): yield {"content": event.delta} + elif isinstance(event, ResponseReasoningSummaryTextDeltaEvent): + # OpenAI can output several reasoning summaries + # in a single ResponseReasoningItem. We split them as separate + # AssistantContent messages. Only last of them will have + # the reasoning `native` field set. + if ( + last_summary_index is not None + and event.summary_index != last_summary_index + ): + yield {"role": "assistant"} + last_role = "assistant" + last_summary_index = event.summary_index + yield {"thinking_content": event.delta} elif isinstance(event, ResponseFunctionCallArgumentsDeltaEvent): current_tool_call.arguments += event.delta elif isinstance(event, ResponseFunctionCallArgumentsDoneEvent): @@ -299,6 +433,33 @@ class OpenAIBaseLLMEntity(Entity): """Generate an answer for the chat log.""" options = self.subentry.data + messages = _convert_content_to_param(chat_log.content) + + model_args = ResponseCreateParamsStreaming( + model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), + input=messages, + max_output_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS), + top_p=options.get(CONF_TOP_P, RECOMMENDED_TOP_P), + temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE), + user=chat_log.conversation_id, + store=False, + stream=True, + ) + + if model_args["model"].startswith(("o", "gpt-5")): + model_args["reasoning"] = { + "effort": options.get( + CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT + ), + "summary": "auto", + } + model_args["include"] = ["reasoning.encrypted_content"] + + if model_args["model"].startswith("gpt-5"): + model_args["text"] = { + "verbosity": options.get(CONF_VERBOSITY, RECOMMENDED_VERBOSITY) + } + tools: list[ToolParam] = [] if chat_log.llm_api: tools = [ @@ -332,35 +493,11 @@ class OpenAIBaseLLMEntity(Entity): ), ) ) + model_args.setdefault("include", []).append("code_interpreter_call.outputs") # type: ignore[union-attr] - model_args = { - "model": options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), - "input": [], - "max_output_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS), - "top_p": options.get(CONF_TOP_P, RECOMMENDED_TOP_P), - "temperature": options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE), - "user": chat_log.conversation_id, - "store": False, - "stream": True, - } if tools: model_args["tools"] = tools - if model_args["model"].startswith("o"): - model_args["reasoning"] = { - "effort": options.get( - CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT - ) - } - else: - model_args["store"] = False - - messages = [ - m - for content in chat_log.content - for m in _convert_content_to_param(content) - ] - last_content = chat_log.content[-1] # Handle attachments by adding them to the last user message @@ -393,16 +530,19 @@ class OpenAIBaseLLMEntity(Entity): # To prevent infinite loops, we limit the number of iterations for _iteration in range(MAX_TOOL_ITERATIONS): - model_args["input"] = messages - try: - result = await client.responses.create(**model_args) + stream = await client.responses.create(**model_args) - async for content in chat_log.async_add_delta_content_stream( - self.entity_id, _transform_stream(chat_log, result, messages) - ): - if not isinstance(content, conversation.AssistantContent): - messages.extend(_convert_content_to_param(content)) + messages.extend( + _convert_content_to_param( + [ + content + async for content in chat_log.async_add_delta_content_stream( + self.entity_id, _transform_stream(chat_log, stream) + ) + ] + ) + ) except openai.RateLimitError as err: LOGGER.error("Rate limited by OpenAI: %s", err) raise HomeAssistantError("Rate limited or insufficient funds") from err diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json index 5a6d76a396b..38ebe205bd3 100644 --- a/homeassistant/components/openai_conversation/manifest.json +++ b/homeassistant/components/openai_conversation/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/openai_conversation", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["openai==1.93.3"] + "requirements": ["openai==1.99.5"] } diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index a1bf236f19b..304ef8b6bdc 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -121,6 +121,7 @@ "selector": { "reasoning_effort": { "options": { + "minimal": "Minimal", "low": "[%key:common::state::low%]", "medium": "[%key:common::state::medium%]", "high": "[%key:common::state::high%]" @@ -132,6 +133,13 @@ "medium": "[%key:common::state::medium%]", "high": "[%key:common::state::high%]" } + }, + "verbosity": { + "options": { + "low": "[%key:common::state::low%]", + "medium": "[%key:common::state::medium%]", + "high": "[%key:common::state::high%]" + } } }, "services": { diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 9f8840b8487..8251a06bd00 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -48,7 +48,7 @@ async def async_setup_entry( device = hass.data[DOMAIN][config_entry.entry_id] - entity = OpenhomeDevice(hass, device) + entity = OpenhomeDevice(device) async_add_entities([entity]) @@ -100,9 +100,8 @@ class OpenhomeDevice(MediaPlayerEntity): _attr_state = MediaPlayerState.PLAYING _attr_available = True - def __init__(self, hass, device): + def __init__(self, device): """Initialise the Openhome device.""" - self.hass = hass self._device = device self._attr_unique_id = device.uuid() self._source_index = {} diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 87b7860afb5..2860abbe64c 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -51,6 +51,7 @@ from .const import ( ATTR_API_WEATHER, ATTR_API_WEATHER_CODE, ATTR_API_WIND_BEARING, + ATTR_API_WIND_GUST, ATTR_API_WIND_SPEED, ATTRIBUTION, DOMAIN, @@ -93,6 +94,13 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key=ATTR_API_WIND_GUST, + name="Wind gust", + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key=ATTR_API_WIND_BEARING, name="Wind bearing", diff --git a/homeassistant/components/opower/__init__.py b/homeassistant/components/opower/__init__.py index 23c8e7a8136..088083ef5db 100644 --- a/homeassistant/components/opower/__init__.py +++ b/homeassistant/components/opower/__init__.py @@ -2,9 +2,13 @@ from __future__ import annotations +from opower import select_utility + from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from .const import CONF_UTILITY, DOMAIN from .coordinator import OpowerConfigEntry, OpowerCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -12,6 +16,25 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: OpowerConfigEntry) -> bool: """Set up Opower from a config entry.""" + utility_name = entry.data[CONF_UTILITY] + try: + select_utility(utility_name) + except ValueError: + ir.async_create_issue( + hass, + DOMAIN, + f"unsupported_utility_{entry.entry_id}", + is_fixable=True, + severity=ir.IssueSeverity.ERROR, + translation_key="unsupported_utility", + translation_placeholders={"utility": utility_name}, + data={ + "entry_id": entry.entry_id, + "utility": utility_name, + "title": entry.title, + }, + ) + return False coordinator = OpowerCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index a10c5b2d15d..e127824ac19 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.15.1"] + "requirements": ["opower==0.15.2"] } diff --git a/homeassistant/components/opower/repairs.py b/homeassistant/components/opower/repairs.py new file mode 100644 index 00000000000..f78dee32194 --- /dev/null +++ b/homeassistant/components/opower/repairs.py @@ -0,0 +1,44 @@ +"""Repairs for Opower.""" + +from __future__ import annotations + +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + + +class UnsupportedUtilityFixFlow(RepairsFlow): + """Handler for removing a configuration entry that uses an unsupported utility.""" + + def __init__(self, data: dict[str, str]) -> None: + """Initialize.""" + self._entry_id = data["entry_id"] + self._placeholders = data.copy() + self._placeholders.pop("entry_id") + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + await self.hass.config_entries.async_remove(self._entry_id) + return self.async_create_entry(title="", data={}) + + return self.async_show_form( + step_id="confirm", description_placeholders=self._placeholders + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, issue_id: str, data: dict[str, str] | None +) -> RepairsFlow: + """Create flow.""" + assert issue_id.startswith("unsupported_utility") + assert data + return UnsupportedUtilityFixFlow(data) diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index 5bb22699220..813e1185467 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -10,7 +10,7 @@ } }, "credentials": { - "title": "Enter Credentials", + "title": "Enter credentials", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", @@ -34,7 +34,7 @@ }, "mfa_code": { "title": "Enter security code", - "description": "A security code has been sent via your selected method. Please enter it below to complete login.", + "description": "Please enter the security code below to complete login.", "data": { "mfa_code": "Security code" }, @@ -70,6 +70,17 @@ "return_to_grid_migration": { "title": "Return to grid statistics for account: {utility_account_id}", "description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue." + }, + "unsupported_utility": { + "title": "Unsupported utility: {utility}", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::opower::issues::unsupported_utility::title%]", + "description": "The utility `{utility}` used by entry `{title}` is no longer supported by the Opower integration. Select **Submit** to remove this integration entry now." + } + } + } } }, "entity": { diff --git a/homeassistant/components/paperless_ngx/quality_scale.yaml b/homeassistant/components/paperless_ngx/quality_scale.yaml index 827d4425132..f0d3296da10 100644 --- a/homeassistant/components/paperless_ngx/quality_scale.yaml +++ b/homeassistant/components/paperless_ngx/quality_scale.yaml @@ -50,19 +50,19 @@ rules: discovery: status: exempt comment: Paperless does not support discovery. - docs-data-update: todo - docs-examples: todo - docs-known-limitations: todo - docs-supported-devices: todo - docs-supported-functions: todo - docs-troubleshooting: todo - docs-use-cases: todo + docs-data-update: done + docs-examples: done + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done dynamic-devices: status: exempt comment: Service type integration entity-category: done entity-device-class: done - entity-disabled-by-default: todo + entity-disabled-by-default: done entity-translations: done exception-translations: done icon-translations: done diff --git a/homeassistant/components/paperless_ngx/sensor.py b/homeassistant/components/paperless_ngx/sensor.py index 5d6bfe1347e..fd066f23240 100644 --- a/homeassistant/components/paperless_ngx/sensor.py +++ b/homeassistant/components/paperless_ngx/sensor.py @@ -56,24 +56,28 @@ SENSOR_STATISTICS: tuple[PaperlessEntityDescription[Statistic], ...] = ( translation_key="characters_count", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.character_count, + entity_registry_enabled_default=False, ), PaperlessEntityDescription[Statistic]( key="tag_count", translation_key="tag_count", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.tag_count, + entity_registry_enabled_default=False, ), PaperlessEntityDescription[Statistic]( key="correspondent_count", translation_key="correspondent_count", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.correspondent_count, + entity_registry_enabled_default=False, ), PaperlessEntityDescription[Statistic]( key="document_type_count", translation_key="document_type_count", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.document_type_count, + entity_registry_enabled_default=False, ), ) @@ -141,6 +145,7 @@ SENSOR_STATUS: tuple[PaperlessEntityDescription[Status], ...] = ( translation_key="index_status", device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, options=[ item.value.lower() for item in StatusType if item != StatusType.UNKNOWN ], @@ -159,6 +164,7 @@ SENSOR_STATUS: tuple[PaperlessEntityDescription[Status], ...] = ( translation_key="classifier_status", device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, options=[ item.value.lower() for item in StatusType if item != StatusType.UNKNOWN ], @@ -177,6 +183,7 @@ SENSOR_STATUS: tuple[PaperlessEntityDescription[Status], ...] = ( translation_key="celery_status", device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, options=[ item.value.lower() for item in StatusType if item != StatusType.UNKNOWN ], diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index a568d51e5ea..779452b284b 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -82,7 +82,7 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN): ) await hub.getSystem() - await hub.setTransport(hub.secured_transport) + await hub.setTransport(hub.secured_transport, hub.api_version_detected) if not hub.system or not hub.name: raise ConnectionFailure("System data or name is empty") diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index f73b7156d3e..ae51fe166c4 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -217,6 +217,13 @@ async def determine_api_version( _LOGGER.debug( "Connection to %s failed: %s, trying API version 5", holeV6.base_url, ex_v6 ) + else: + # It seems that occasionally the auth can succeed unexpectedly when there is a valid session + _LOGGER.warning( + "Authenticated with %s through v6 API, but succeeded with an incorrect password. This is a known bug", + holeV6.base_url, + ) + return 6 holeV5 = api_by_version(hass, entry, 5, password="wrong_token") try: await holeV5.get_data() diff --git a/homeassistant/components/plaato/coordinator.py b/homeassistant/components/plaato/coordinator.py index df360d50068..74ff8566729 100644 --- a/homeassistant/components/plaato/coordinator.py +++ b/homeassistant/components/plaato/coordinator.py @@ -31,7 +31,6 @@ class PlaatoCoordinator(DataUpdateCoordinator): ) -> None: """Initialize.""" self.api = Plaato(auth_token=auth_token) - self.hass = hass self.device_type = device_type self.platforms: list[Platform] = [] diff --git a/homeassistant/components/playstation_network/config_flow.py b/homeassistant/components/playstation_network/config_flow.py index d4822225c61..d7d82292378 100644 --- a/homeassistant/components/playstation_network/config_flow.py +++ b/homeassistant/components/playstation_network/config_flow.py @@ -202,6 +202,9 @@ class FriendSubentryFlowHandler(ConfigSubentryFlow): } ) + if not self.friends_list: + return self.async_abort(reason="no_friends") + options = [ SelectOptionDict( value=friend.account_id, @@ -209,6 +212,7 @@ class FriendSubentryFlowHandler(ConfigSubentryFlow): ) for friend in self.friends_list.values() ] + return self.async_show_form( step_id="user", data_schema=self.add_suggested_values_to_schema( diff --git a/homeassistant/components/playstation_network/strings.json b/homeassistant/components/playstation_network/strings.json index 26a1b336e2d..15b83b7cd0d 100644 --- a/homeassistant/components/playstation_network/strings.json +++ b/homeassistant/components/playstation_network/strings.json @@ -69,7 +69,8 @@ }, "abort": { "already_configured_as_entry": "Already configured as a service. This account cannot be added as a friend.", - "already_configured": "Already configured as a friend in this or another account." + "already_configured": "Already configured as a friend in this or another account.", + "no_friends": "Looks like your friend list is empty right now. Add friends on PlayStation Network first." } } }, diff --git a/homeassistant/components/point/entity.py b/homeassistant/components/point/entity.py index 39af7867e97..b6718d7fd2d 100644 --- a/homeassistant/components/point/entity.py +++ b/homeassistant/components/point/entity.py @@ -28,8 +28,9 @@ class MinutPointEntity(CoordinatorEntity[PointDataUpdateCoordinator]): connections={(dr.CONNECTION_NETWORK_MAC, device["device_mac"])}, identifiers={(DOMAIN, device["device_id"])}, manufacturer="Minut", - model=f"Point v{device['hardware_version']}", + model="Point", name=device["description"], + hw_version=device["hardware_version"], sw_version=device["firmware"]["installed"], via_device=(DOMAIN, device["home"]), ) diff --git a/homeassistant/components/powerfox/__init__.py b/homeassistant/components/powerfox/__init__.py index 8e51985211d..c2f6830692c 100644 --- a/homeassistant/components/powerfox/__init__.py +++ b/homeassistant/components/powerfox/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio -from powerfox import Powerfox, PowerfoxConnectionError +from powerfox import DeviceType, Powerfox, PowerfoxConnectionError from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant @@ -31,7 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) -> raise ConfigEntryNotReady from err coordinators: list[PowerfoxDataUpdateCoordinator] = [ - PowerfoxDataUpdateCoordinator(hass, entry, client, device) for device in devices + PowerfoxDataUpdateCoordinator(hass, entry, client, device) + for device in devices + # Filter out gas meter devices (Powerfox FLOW adapters) as they are not yet supported and cause integration failures + if device.type != DeviceType.GAS_METER ] await asyncio.gather( diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index 8818eff2d81..826d5872d7c 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -30,9 +30,9 @@ async def validate_input(hass: HomeAssistant, data): return { "title": is_valid["title"], - "relay_count": is_valid["relay_count"], - "input_count": is_valid["input_count"], - "is_old": is_valid["is_old"], + "relay_count": is_valid["relays"], + "input_count": is_valid["inputs"], + "is_old": is_valid["temps"], } diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 3adc33e9935..ac0e8f249f5 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -366,6 +366,7 @@ class PrometheusMetrics: @staticmethod def _sanitize_metric_name(metric: str) -> str: + metric.replace("\u03bc", "\u00b5") return "".join( [c if c in ALLOWED_METRIC_CHARS else f"u{hex(ord(c))}" for c in metric] ) @@ -747,6 +748,9 @@ class PrometheusMetrics: PERCENTAGE: "percent", } default = unit.replace("/", "_per_") + # Unit conversion for CONCENTRATION_MICROGRAMS_PER_CUBIC_METER "μg/m³" + # "μ" == "\u03bc" but the API uses "\u00b5" + default = default.replace("\u03bc", "\u00b5") default = default.lower() return units.get(unit, default) diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 4d120e9fae7..ad35e409627 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,18 +1,17 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .const import ATTR_POWER, ATTR_POWER_P3, DOMAIN -from .coordinator import ElecPricesDataUpdateCoordinator +from .const import ATTR_POWER, ATTR_POWER_P3 +from .coordinator import ElecPricesDataUpdateCoordinator, PVPCConfigEntry from .helpers import get_enabled_sensor_keys PLATFORMS: list[Platform] = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: PVPCConfigEntry) -> bool: """Set up pvpc hourly pricing from a config entry.""" entity_registry = er.async_get(hass) sensor_keys = get_enabled_sensor_keys( @@ -22,13 +21,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = ElecPricesDataUpdateCoordinator(hass, entry, sensor_keys) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True -async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_update_options(hass: HomeAssistant, entry: PVPCConfigEntry) -> None: """Handle options update.""" if any( entry.data.get(attrib) != entry.options.get(attrib) @@ -41,9 +40,6 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: PVPCConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/pvpc_hourly_pricing/coordinator.py b/homeassistant/components/pvpc_hourly_pricing/coordinator.py index 28e676d37ed..bc9d6a21557 100644 --- a/homeassistant/components/pvpc_hourly_pricing/coordinator.py +++ b/homeassistant/components/pvpc_hourly_pricing/coordinator.py @@ -17,14 +17,16 @@ from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN _LOGGER = logging.getLogger(__name__) +type PVPCConfigEntry = ConfigEntry[ElecPricesDataUpdateCoordinator] + class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]): """Class to manage fetching Electricity prices data from API.""" - config_entry: ConfigEntry + config_entry: PVPCConfigEntry def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, sensor_keys: set[str] + self, hass: HomeAssistant, entry: PVPCConfigEntry, sensor_keys: set[str] ) -> None: """Initialize.""" self.api = PVPCData( diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 1b92cfc533d..c49756290ab 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CURRENCY_EURO, UnitOfEnergy from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -24,7 +23,7 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import ElecPricesDataUpdateCoordinator +from .coordinator import ElecPricesDataUpdateCoordinator, PVPCConfigEntry from .helpers import make_sensor_unique_id _LOGGER = logging.getLogger(__name__) @@ -149,11 +148,11 @@ _PRICE_SENSOR_ATTRIBUTES_MAP = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: PVPCConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the electricity price sensor from config_entry.""" - coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data sensors = [ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id)] if coordinator.api.using_private_api: sensors.extend( diff --git a/homeassistant/components/qbus/binary_sensor.py b/homeassistant/components/qbus/binary_sensor.py new file mode 100644 index 00000000000..d91b6c9cbe6 --- /dev/null +++ b/homeassistant/components/qbus/binary_sensor.py @@ -0,0 +1,144 @@ +"""Support for Qbus binary sensor.""" + +from dataclasses import dataclass +from typing import cast + +from qbusmqttapi.discovery import QbusMqttDevice, QbusMqttOutput +from qbusmqttapi.factory import QbusMqttTopicFactory +from qbusmqttapi.state import QbusMqttDeviceState, QbusMqttWeatherState + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .coordinator import QbusConfigEntry +from .entity import ( + QbusEntity, + create_device_identifier, + create_unique_id, + determine_new_outputs, +) + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class QbusWeatherDescription(BinarySensorEntityDescription): + """Description for Qbus weather entities.""" + + property: str + + +_WEATHER_DESCRIPTIONS = ( + QbusWeatherDescription( + key="raining", + property="raining", + translation_key="raining", + ), + QbusWeatherDescription( + key="twilight", + property="twilight", + translation_key="twilight", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: QbusConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up binary sensor entities.""" + + coordinator = entry.runtime_data + added_outputs: list[QbusMqttOutput] = [] + added_controllers: list[str] = [] + + def _create_weather_entities() -> list[BinarySensorEntity]: + new_outputs = determine_new_outputs( + coordinator, added_outputs, lambda output: output.type == "weatherstation" + ) + + return [ + QbusWeatherBinarySensor(output, description) + for output in new_outputs + for description in _WEATHER_DESCRIPTIONS + ] + + def _create_controller_entities() -> list[BinarySensorEntity]: + if coordinator.data and coordinator.data.id not in added_controllers: + added_controllers.extend(coordinator.data.id) + return [QbusControllerConnectedBinarySensor(coordinator.data)] + + return [] + + def _check_outputs() -> None: + entities = [*_create_weather_entities(), *_create_controller_entities()] + async_add_entities(entities) + + _check_outputs() + entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) + + +class QbusWeatherBinarySensor(QbusEntity, BinarySensorEntity): + """Representation of a Qbus weather binary sensor.""" + + _state_cls = QbusMqttWeatherState + + entity_description: QbusWeatherDescription + + def __init__( + self, mqtt_output: QbusMqttOutput, description: QbusWeatherDescription + ) -> None: + """Initialize binary sensor entity.""" + + super().__init__(mqtt_output, id_suffix=description.key) + + self.entity_description = description + + async def _handle_state_received(self, state: QbusMqttWeatherState) -> None: + if value := state.read_property(self.entity_description.property, None): + self._attr_is_on = ( + None if value is None else cast(str, value).lower() == "true" + ) + + +class QbusControllerConnectedBinarySensor(BinarySensorEntity): + """Representation of the Qbus controller connected sensor.""" + + _attr_has_entity_name = True + _attr_name = None + _attr_should_poll = False + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + + def __init__(self, controller: QbusMqttDevice) -> None: + """Initialize binary sensor entity.""" + self._controller = controller + + self._attr_unique_id = create_unique_id(controller.serial_number, "connected") + self._attr_device_info = DeviceInfo( + identifiers={create_device_identifier(controller)} + ) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + topic = QbusMqttTopicFactory().get_device_state_topic(self._controller.id) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{topic}", + self._state_received, + ) + ) + + @callback + def _state_received(self, state: QbusMqttDeviceState) -> None: + self._attr_is_on = state.properties.connected if state.properties else None + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/qbus/const.py b/homeassistant/components/qbus/const.py index 133a3b8fea9..3ecab64059a 100644 --- a/homeassistant/components/qbus/const.py +++ b/homeassistant/components/qbus/const.py @@ -6,6 +6,7 @@ from homeassistant.const import Platform DOMAIN: Final = "qbus" PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.COVER, Platform.LIGHT, diff --git a/homeassistant/components/qbus/coordinator.py b/homeassistant/components/qbus/coordinator.py index 42e226c8e6a..c3fbf4b60bb 100644 --- a/homeassistant/components/qbus/coordinator.py +++ b/homeassistant/components/qbus/coordinator.py @@ -6,7 +6,7 @@ from datetime import datetime import logging from typing import cast -from qbusmqttapi.discovery import QbusDiscovery, QbusMqttDevice, QbusMqttOutput +from qbusmqttapi.discovery import QbusDiscovery, QbusMqttDevice from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory from homeassistant.components.mqtt import ( @@ -19,6 +19,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.hass_dict import HassKey @@ -32,7 +33,7 @@ type QbusConfigEntry = ConfigEntry[QbusControllerCoordinator] QBUS_KEY: HassKey[QbusConfigCoordinator] = HassKey(DOMAIN) -class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]): +class QbusControllerCoordinator(DataUpdateCoordinator[QbusMqttDevice | None]): """Qbus data coordinator.""" _STATE_REQUEST_DELAY = 3 @@ -63,8 +64,8 @@ class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) ) - async def _async_update_data(self) -> list[QbusMqttOutput]: - return self._controller.outputs if self._controller else [] + async def _async_update_data(self) -> QbusMqttDevice | None: + return self._controller def shutdown(self, event: Event | None = None) -> None: """Shutdown Qbus coordinator.""" @@ -140,20 +141,25 @@ class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]): "%s - Receiving controller state %s", self.config_entry.unique_id, msg.topic ) - if self._controller is None or self._controller_activated: + if self._controller is None: return state = self._message_factory.parse_device_state(msg.payload) - if state and state.properties and state.properties.connectable is False: - _LOGGER.debug( - "%s - Activating controller %s", self.config_entry.unique_id, state.id - ) - self._controller_activated = True - request = self._message_factory.create_device_activate_request( - self._controller - ) - await mqtt.async_publish(self.hass, request.topic, request.payload) + if state and state.properties: + async_dispatcher_send(self.hass, f"{DOMAIN}_{msg.topic}", state) + + if not self._controller_activated and state.properties.connectable is False: + _LOGGER.debug( + "%s - Activating controller %s", + self.config_entry.unique_id, + state.id, + ) + self._controller_activated = True + request = self._message_factory.create_device_activate_request( + self._controller + ) + await mqtt.async_publish(self.hass, request.topic, request.payload) def _request_entity_states(self) -> None: async def request_state(_: datetime) -> None: diff --git a/homeassistant/components/qbus/entity.py b/homeassistant/components/qbus/entity.py index 9fb481d4515..f7205a85c00 100644 --- a/homeassistant/components/qbus/entity.py +++ b/homeassistant/components/qbus/entity.py @@ -7,7 +7,7 @@ from collections.abc import Callable import re from typing import Generic, TypeVar, cast -from qbusmqttapi.discovery import QbusMqttOutput +from qbusmqttapi.discovery import QbusMqttDevice, QbusMqttOutput from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory from qbusmqttapi.state import QbusMqttState @@ -44,11 +44,15 @@ def determine_new_outputs( added_ref_ids = {k.ref_id for k in added_outputs} - new_outputs = [ - output - for output in coordinator.data - if filter_fn(output) and output.ref_id not in added_ref_ids - ] + new_outputs = ( + [ + output + for output in coordinator.data.outputs + if filter_fn(output) and output.ref_id not in added_ref_ids + ] + if coordinator.data + else [] + ) if new_outputs: added_outputs.extend(new_outputs) @@ -64,9 +68,14 @@ def format_ref_id(ref_id: str) -> str | None: return None -def create_main_device_identifier(mqtt_output: QbusMqttOutput) -> tuple[str, str]: - """Create the identifier referring to the main device this output belongs to.""" - return (DOMAIN, format_mac(mqtt_output.device.mac)) +def create_device_identifier(mqtt_device: QbusMqttDevice) -> tuple[str, str]: + """Create the device identifier.""" + return (DOMAIN, format_mac(mqtt_device.mac)) + + +def create_unique_id(serial_number: str, suffix: str) -> str: + """Create the unique id.""" + return f"ctd_{serial_number}_{suffix}" class QbusEntity(Entity, Generic[StateT], ABC): @@ -95,16 +104,18 @@ class QbusEntity(Entity, Generic[StateT], ABC): ) ref_id = format_ref_id(mqtt_output.ref_id) - unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}" + suffix = ref_id or "" if id_suffix: - unique_id += f"_{id_suffix}" + suffix += f"_{id_suffix}" - self._attr_unique_id = unique_id + self._attr_unique_id = create_unique_id( + mqtt_output.device.serial_number, suffix + ) if link_to_main_device: self._attr_device_info = DeviceInfo( - identifiers={create_main_device_identifier(mqtt_output)} + identifiers={create_device_identifier(mqtt_output.device)} ) else: self._attr_device_info = DeviceInfo( @@ -112,7 +123,7 @@ class QbusEntity(Entity, Generic[StateT], ABC): manufacturer=MANUFACTURER, identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")}, suggested_area=mqtt_output.location.title(), - via_device=create_main_device_identifier(mqtt_output), + via_device=create_device_identifier(mqtt_output.device), ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/qbus/icons.json b/homeassistant/components/qbus/icons.json new file mode 100644 index 00000000000..400a2bba935 --- /dev/null +++ b/homeassistant/components/qbus/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "binary_sensor": { + "raining": { + "default": "mdi:weather-pouring" + }, + "twilight": { + "default": "mdi:weather-sunset" + } + } + } +} diff --git a/homeassistant/components/qbus/strings.json b/homeassistant/components/qbus/strings.json index f3a0d108476..87788787baa 100644 --- a/homeassistant/components/qbus/strings.json +++ b/homeassistant/components/qbus/strings.json @@ -17,6 +17,14 @@ } }, "entity": { + "binary_sensor": { + "raining": { + "name": "Raining" + }, + "twilight": { + "name": "Twilight" + } + }, "sensor": { "daylight": { "name": "Daylight" diff --git a/homeassistant/components/rachio/coordinator.py b/homeassistant/components/rachio/coordinator.py index 62d42f2afda..6d482e9c900 100644 --- a/homeassistant/components/rachio/coordinator.py +++ b/homeassistant/components/rachio/coordinator.py @@ -44,7 +44,6 @@ class RachioUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): base_count: int, ) -> None: """Initialize the Rachio Update Coordinator.""" - self.hass = hass self.rachio = rachio self.base_station = base_station super().__init__( @@ -83,7 +82,6 @@ class RachioScheduleUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]] base_station, ) -> None: """Initialize a Rachio schedule coordinator.""" - self.hass = hass self.rachio = rachio self.base_station = base_station super().__init__( diff --git a/homeassistant/components/rainmachine/entity.py b/homeassistant/components/rainmachine/entity.py index 1289d3e808e..441cf8237b6 100644 --- a/homeassistant/components/rainmachine/entity.py +++ b/homeassistant/components/rainmachine/entity.py @@ -56,11 +56,9 @@ class RainMachineEntity(CoordinatorEntity[RainMachineDataUpdateCoordinator]): connections={(dr.CONNECTION_NETWORK_MAC, self._data.controller.mac)}, name=self._data.controller.name.capitalize(), manufacturer="RainMachine", - model=( - f"Version {self._version_coordinator.data['hwVer']} " - f"(API: {self._version_coordinator.data['apiVer']})" - ), - sw_version=self._version_coordinator.data["swVer"], + hw_version=self._version_coordinator.data["hwVer"], + sw_version=f"{self._version_coordinator.data['swVer']} " + f"(API: {self._version_coordinator.data['apiVer']})", ) @callback diff --git a/homeassistant/components/recorder/auto_repairs/schema.py b/homeassistant/components/recorder/auto_repairs/schema.py index e14a165f81f..3952f76bddd 100644 --- a/homeassistant/components/recorder/auto_repairs/schema.py +++ b/homeassistant/components/recorder/auto_repairs/schema.py @@ -261,7 +261,7 @@ def correct_db_schema_precision( from ..migration import _modify_columns # noqa: PLC0415 precision_columns = _get_precision_column_types(table_object) - # Attempt to convert timestamp columns to µs precision + # Attempt to convert timestamp columns to μs precision session_maker = instance.get_session engine = instance.engine assert engine is not None, "Engine should be set" diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7326519b14e..2321da45bb9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -42,6 +42,7 @@ from homeassistant.util import dt as dt_util from homeassistant.util.collection import chunked_or_all from homeassistant.util.enum import try_parse_enum from homeassistant.util.unit_conversion import ( + ApparentPowerConverter, AreaConverter, BaseUnitConverter, BloodGlucoseConcentrationConverter, @@ -59,6 +60,7 @@ from homeassistant.util.unit_conversion import ( PowerConverter, PressureConverter, ReactiveEnergyConverter, + ReactivePowerConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -193,6 +195,7 @@ QUERY_STATISTICS_SUMMARY_SUM = ( STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { + **dict.fromkeys(ApparentPowerConverter.VALID_UNITS, ApparentPowerConverter), **dict.fromkeys(AreaConverter.VALID_UNITS, AreaConverter), **dict.fromkeys( BloodGlucoseConcentrationConverter.VALID_UNITS, @@ -214,6 +217,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { **dict.fromkeys(PowerConverter.VALID_UNITS, PowerConverter), **dict.fromkeys(PressureConverter.VALID_UNITS, PressureConverter), **dict.fromkeys(ReactiveEnergyConverter.VALID_UNITS, ReactiveEnergyConverter), + **dict.fromkeys(ReactivePowerConverter.VALID_UNITS, ReactivePowerConverter), **dict.fromkeys(SpeedConverter.VALID_UNITS, SpeedConverter), **dict.fromkeys(TemperatureConverter.VALID_UNITS, TemperatureConverter), **dict.fromkeys(UnitlessRatioConverter.VALID_UNITS, UnitlessRatioConverter), diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index d052631c5f6..4f798fb86d0 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -16,6 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_bytes from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( + ApparentPowerConverter, AreaConverter, BloodGlucoseConcentrationConverter, ConductivityConverter, @@ -32,6 +33,7 @@ from homeassistant.util.unit_conversion import ( PowerConverter, PressureConverter, ReactiveEnergyConverter, + ReactivePowerConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -59,6 +61,7 @@ UPDATE_STATISTICS_METADATA_TIME_OUT = 10 UNIT_SCHEMA = vol.Schema( { + vol.Optional("apparent_power"): vol.In(ApparentPowerConverter.VALID_UNITS), vol.Optional("area"): vol.In(AreaConverter.VALID_UNITS), vol.Optional("blood_glucose_concentration"): vol.In( BloodGlucoseConcentrationConverter.VALID_UNITS @@ -79,6 +82,7 @@ UNIT_SCHEMA = vol.Schema( vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS), + vol.Optional("reactive_power"): vol.In(ReactivePowerConverter.VALID_UNITS), vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS), diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 2861c52c24a..9fe01c5b952 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "silver", - "requirements": ["renault-api==0.3.1"] + "requirements": ["renault-api==0.4.0"] } diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py index 1f883435dee..5e14328eb7c 100644 --- a/homeassistant/components/renault/renault_hub.py +++ b/homeassistant/components/renault/renault_hub.py @@ -156,6 +156,7 @@ class RenaultHub: name=vehicle.device_info[ATTR_NAME], model=vehicle.device_info[ATTR_MODEL], model_id=vehicle.device_info[ATTR_MODEL_ID], + sw_version=None, # cleanup from PR #125399 ) self._vehicles[vehicle_link.vin] = vehicle diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 4117b0ee35b..d09c567bb71 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -137,9 +137,9 @@ class RepairsFlowIndexView(FlowManagerIndexView): "Handler does not support user", HTTPStatus.BAD_REQUEST ) - result = self._prepare_result_json(result) - - return self.json(result) + return self.json( + self._prepare_result_json(result), + ) class RepairsFlowResourceView(FlowManagerResourceView): diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 3341f296fb9..2964ef73d46 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -45,6 +45,7 @@ class RestData: self._method = method self._resource = resource self._encoding = encoding + self._force_use_set_encoding = False # Convert auth tuple to aiohttp.BasicAuth if needed if isinstance(auth, tuple) and len(auth) == 2: @@ -152,10 +153,19 @@ class RestData: # Read the response # Only use configured encoding if no charset in Content-Type header # If charset is present in Content-Type, let aiohttp use it - if response.charset: + if self._force_use_set_encoding is False and response.charset: # Let aiohttp use the charset from Content-Type header - self.data = await response.text() - else: + try: + self.data = await response.text() + except UnicodeDecodeError as ex: + self._force_use_set_encoding = True + _LOGGER.debug( + "Response charset came back as %s but could not be decoded, continue with configured encoding %s. %s", + response.charset, + self._encoding, + ex, + ) + if self._force_use_set_encoding or not response.charset: # Use configured encoding as fallback self.data = await response.text(encoding=self._encoding) self.headers = response.headers diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 4bf3c49a726..afdb3b19cb4 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -148,10 +148,14 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity): async def async_start(self) -> None: """Start the vacuum.""" - if self._device_status.in_cleaning == 2: + if self._device_status.in_returning == 1: + await self.send(RoborockCommand.APP_CHARGE) + elif self._device_status.in_cleaning == 2: await self.send(RoborockCommand.RESUME_ZONED_CLEAN) elif self._device_status.in_cleaning == 3: await self.send(RoborockCommand.RESUME_SEGMENT_CLEAN) + elif self._device_status.in_cleaning == 4: + await self.send(RoborockCommand.APP_RESUME_BUILD_MAP) else: await self.send(RoborockCommand.APP_START) diff --git a/homeassistant/components/romy/coordinator.py b/homeassistant/components/romy/coordinator.py index d666ec44f80..de5352191d7 100644 --- a/homeassistant/components/romy/coordinator.py +++ b/homeassistant/components/romy/coordinator.py @@ -25,7 +25,6 @@ class RomyVacuumCoordinator(DataUpdateCoordinator[None]): name=DOMAIN, update_interval=UPDATE_INTERVAL, ) - self.hass = hass self.romy = romy async def _async_update_data(self) -> None: diff --git a/homeassistant/components/roomba/entity.py b/homeassistant/components/roomba/entity.py index 14c7ac3af3e..eb1b3696102 100644 --- a/homeassistant/components/roomba/entity.py +++ b/homeassistant/components/roomba/entity.py @@ -51,11 +51,6 @@ class IRobotEntity(Entity): """Return the uniqueid of the vacuum cleaner.""" return self.robot_unique_id - @property - def battery_level(self): - """Return the battery level of the vacuum cleaner.""" - return self.vacuum_state.get("batPct") - @property def run_stats(self): """Return the run stats.""" diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 3a98bedcd94..ae82424ec34 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -35,7 +35,7 @@ SENSORS: list[RoombaSensorEntityDescription] = [ native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda self: self.battery_level, + value_fn=lambda self: self.vacuum_state.get("batPct"), ), RoombaSensorEntityDescription( key="battery_cycles", diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index 10606814a35..0c24301f2af 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -24,8 +24,7 @@ from .entity import IRobotEntity from .models import RoombaData SUPPORT_IROBOT = ( - VacuumEntityFeature.BATTERY - | VacuumEntityFeature.PAUSE + VacuumEntityFeature.PAUSE | VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.START diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index aad9b9425aa..efaf8f195ad 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==4.8.0"], + "requirements": ["aiorussound==4.8.1"], "zeroconf": ["_rio._tcp.local."] } diff --git a/homeassistant/components/russound_rio/media_browser.py b/homeassistant/components/russound_rio/media_browser.py new file mode 100644 index 00000000000..7e5ca741f90 --- /dev/null +++ b/homeassistant/components/russound_rio/media_browser.py @@ -0,0 +1,97 @@ +"""Support for Russound media browsing.""" + +from aiorussound import RussoundClient, Zone +from aiorussound.const import FeatureFlag +from aiorussound.util import is_feature_supported + +from homeassistant.components.media_player import BrowseMedia, MediaClass +from homeassistant.core import HomeAssistant + + +async def async_browse_media( + hass: HomeAssistant, + client: RussoundClient, + media_content_id: str | None, + media_content_type: str | None, + zone: Zone, +) -> BrowseMedia: + """Browse media.""" + if media_content_type == "presets": + return await _presets_payload(_find_presets_by_zone(client, zone)) + + return await _root_payload(hass, _find_presets_by_zone(client, zone)) + + +async def _root_payload( + hass: HomeAssistant, presets_by_zone: dict[int, dict[int, str]] +) -> BrowseMedia: + """Return root payload for Russound RIO.""" + children: list[BrowseMedia] = [] + + if presets_by_zone: + children.append( + BrowseMedia( + title="Presets", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="presets", + thumbnail="https://brands.home-assistant.io/_/russound_rio/logo.png", + can_play=False, + can_expand=True, + ) + ) + + return BrowseMedia( + title="Russound", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="root", + can_play=False, + can_expand=True, + children=children, + ) + + +async def _presets_payload(presets_by_zone: dict[int, dict[int, str]]) -> BrowseMedia: + """Create payload to list presets.""" + children: list[BrowseMedia] = [] + for source_id, presets in presets_by_zone.items(): + for preset_id, preset_name in presets.items(): + children.append( + BrowseMedia( + title=preset_name, + media_class=MediaClass.CHANNEL, + media_content_id=f"{source_id},{preset_id}", + media_content_type="preset", + can_play=True, + can_expand=False, + ) + ) + + return BrowseMedia( + title="Presets", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="presets", + can_play=False, + can_expand=True, + children=children, + ) + + +def _find_presets_by_zone( + client: RussoundClient, zone: Zone +) -> dict[int, dict[int, str]]: + """Returns a dict by {source_id: {preset_id: preset_name}}.""" + assert client.rio_version + return { + source_id: source.presets + for source_id, source in client.sources.items() + if source.presets + and ( + not is_feature_supported( + client.rio_version, FeatureFlag.SUPPORT_ZONE_SOURCE_EXCLUSION + ) + or source_id in zone.enabled_sources + ) + } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index a4b86a85e94..a09c663a983 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -13,6 +13,7 @@ from aiorussound.models import PlayStatus, Source from aiorussound.util import is_feature_supported from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -23,7 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import RussoundConfigEntry +from . import RussoundConfigEntry, media_browser from .const import DOMAIN, RUSSOUND_MEDIA_TYPE_PRESET, SELECT_SOURCE_DELAY from .entity import RussoundBaseEntity, command @@ -65,7 +66,8 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): _attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( - MediaPlayerEntityFeature.VOLUME_SET + MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.TURN_ON @@ -264,3 +266,13 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): translation_placeholders={"preset_id": media_id}, ) await self._zone.restore_preset(preset_id) + + async def async_browse_media( + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the media browsing helper.""" + return await media_browser.async_browse_media( + self.hass, self._client, media_content_id, media_content_type, self._zone + ) diff --git a/homeassistant/components/ruuvitag_ble/manifest.json b/homeassistant/components/ruuvitag_ble/manifest.json index fa8ec80423c..1051c9613a6 100644 --- a/homeassistant/components/ruuvitag_ble/manifest.json +++ b/homeassistant/components/ruuvitag_ble/manifest.json @@ -16,5 +16,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/ruuvitag_ble", "iot_class": "local_push", - "requirements": ["ruuvitag-ble==0.1.2"] + "requirements": ["ruuvitag-ble==0.2.1"] } diff --git a/homeassistant/components/ruuvitag_ble/sensor.py b/homeassistant/components/ruuvitag_ble/sensor.py index 57248d547ba..44311fd12eb 100644 --- a/homeassistant/components/ruuvitag_ble/sensor.py +++ b/homeassistant/components/ruuvitag_ble/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from sensor_state_data import ( DeviceKey, - SensorDescription, SensorDeviceClass as SSDSensorDeviceClass, SensorUpdate, Units, @@ -32,53 +31,108 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .const import DOMAIN SENSOR_DESCRIPTIONS = { - (SSDSensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + "temperature": SensorEntityDescription( key=f"{SSDSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), - (SSDSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + "humidity": SensorEntityDescription( key=f"{SSDSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - (SSDSensorDeviceClass.PRESSURE, Units.PRESSURE_HPA): SensorEntityDescription( + "pressure": SensorEntityDescription( key=f"{SSDSensorDeviceClass.PRESSURE}_{Units.PRESSURE_HPA}", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=UnitOfPressure.HPA, state_class=SensorStateClass.MEASUREMENT, ), - ( - SSDSensorDeviceClass.VOLTAGE, - Units.ELECTRIC_POTENTIAL_MILLIVOLT, - ): SensorEntityDescription( + "voltage": SensorEntityDescription( key=f"{SSDSensorDeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_MILLIVOLT}", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), - ( - SSDSensorDeviceClass.SIGNAL_STRENGTH, - Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - ): SensorEntityDescription( + "signal_strength": SensorEntityDescription( key=f"{SSDSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), - (SSDSensorDeviceClass.COUNT, None): SensorEntityDescription( + "movement_counter": SensorEntityDescription( key="movement_counter", + translation_key="movement_counter", state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), + # Acceleration keys exported in newer versions of ruuvitag-ble + "acceleration_x": SensorEntityDescription( + key=f"acceleration_x_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}", + translation_key="acceleration_x", + native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "acceleration_y": SensorEntityDescription( + key=f"acceleration_y_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}", + translation_key="acceleration_y", + native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "acceleration_z": SensorEntityDescription( + key=f"acceleration_z_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}", + translation_key="acceleration_z", + native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "acceleration_total": SensorEntityDescription( + key=f"acceleration_total_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}", + translation_key="acceleration_total", + native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + # Keys exported for dataformat 06 sensors in newer versions of ruuvitag-ble + "pm25": SensorEntityDescription( + key=f"{SSDSensorDeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + "carbon_dioxide": SensorEntityDescription( + key=f"{SSDSensorDeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=Units.CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + "illuminance": SensorEntityDescription( + key=f"{SSDSensorDeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=Units.LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + "voc_index": SensorEntityDescription( + key="voc_index", + translation_key="voc_index", + state_class=SensorStateClass.MEASUREMENT, + ), + "nox_index": SensorEntityDescription( + key="nox_index", + translation_key="nox_index", + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -89,37 +143,28 @@ def _device_key_to_bluetooth_entity_key( return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) -def _to_sensor_key( - description: SensorDescription, -) -> tuple[SSDSensorDeviceClass, Units | None]: - assert description.device_class is not None - return (description.device_class, description.native_unit_of_measurement) - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" + entity_descriptions: dict[PassiveBluetoothEntityKey, EntityDescription] = {} + entity_data = {} + for device_key, sensor_values in sensor_update.entity_values.items(): + bek = _device_key_to_bluetooth_entity_key(device_key) + entity_data[bek] = sensor_values.native_value + for device_key in sensor_update.entity_descriptions: + bek = _device_key_to_bluetooth_entity_key(device_key) + if sk_description := SENSOR_DESCRIPTIONS.get(device_key.key): + entity_descriptions[bek] = sk_description + return PassiveBluetoothDataUpdate( devices={ device_id: sensor_device_info_to_hass_device_info(device_info) for device_id, device_info in sensor_update.devices.items() }, - entity_descriptions={ - _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ - _to_sensor_key(description) - ] - for device_key, description in sensor_update.entity_descriptions.items() - if _to_sensor_key(description) in SENSOR_DESCRIPTIONS - }, - entity_data={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value - for device_key, sensor_values in sensor_update.entity_values.items() - }, - entity_names={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.name - for device_key, sensor_values in sensor_update.entity_values.items() - }, + entity_descriptions=entity_descriptions, + entity_data=entity_data, + entity_names={}, ) diff --git a/homeassistant/components/ruuvitag_ble/strings.json b/homeassistant/components/ruuvitag_ble/strings.json index 16a80220a20..0abb8343c65 100644 --- a/homeassistant/components/ruuvitag_ble/strings.json +++ b/homeassistant/components/ruuvitag_ble/strings.json @@ -18,5 +18,30 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "acceleration_total": { + "name": "Acceleration total" + }, + "acceleration_x": { + "name": "Acceleration X" + }, + "acceleration_y": { + "name": "Acceleration Y" + }, + "acceleration_z": { + "name": "Acceleration Z" + }, + "movement_counter": { + "name": "Movement counter" + }, + "nox_index": { + "name": "NOx index" + }, + "voc_index": { + "name": "VOC index" + } + } } } diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index ea569f4e277..63de3daaf15 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -242,7 +242,7 @@ class ScheduleStorageCollection(DictStorageCollection): async def _update_data(self, item: dict, update_data: dict) -> dict: """Return a new updated data object.""" self.SCHEMA(update_data) - return item | update_data + return {CONF_ID: item[CONF_ID]} | update_data async def _async_load_data(self) -> SerializedStorageCollection | None: """Load the data.""" diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 8b9d7ddf37e..1fddfe6c8f1 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", - "requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.0"] + "requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.1"] } diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index 5a0546b1aa2..1ed9a1bbefc 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -8,24 +8,11 @@ from typing import TYPE_CHECKING, Any from pysensibo.model import SensiboDevice -from homeassistant.components.automation import automations_with_entity -from homeassistant.components.script import scripts_with_entity -from homeassistant.components.select import ( - DOMAIN as SELECT_DOMAIN, - SelectEntity, - SelectEntityDescription, -) +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) from . import SensiboConfigEntry -from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity, async_handle_api_call @@ -42,16 +29,6 @@ class SensiboSelectEntityDescription(SelectEntityDescription): transformation: Callable[[SensiboDevice], dict | None] -HORIZONTAL_SWING_MODE_TYPE = SensiboSelectEntityDescription( - key="horizontalSwing", - data_key="horizontal_swing_mode", - value_fn=lambda data: data.horizontal_swing_mode, - options_fn=lambda data: data.horizontal_swing_modes, - translation_key="horizontalswing", - transformation=lambda data: data.horizontal_swing_modes_translated, - entity_registry_enabled_default=False, -) - DEVICE_SELECT_TYPES = ( SensiboSelectEntityDescription( key="light", @@ -73,43 +50,6 @@ async def async_setup_entry( coordinator = entry.runtime_data - entities: list[SensiboSelect] = [] - - entity_registry = er.async_get(hass) - for device_id, device_data in coordinator.data.parsed.items(): - if entity_id := entity_registry.async_get_entity_id( - SELECT_DOMAIN, DOMAIN, f"{device_id}-horizontalSwing" - ): - entity = entity_registry.async_get(entity_id) - if entity and entity.disabled: - entity_registry.async_remove(entity_id) - async_delete_issue( - hass, - DOMAIN, - "deprecated_entity_horizontalswing", - ) - elif entity and HORIZONTAL_SWING_MODE_TYPE.key in device_data.full_features: - entities.append( - SensiboSelect(coordinator, device_id, HORIZONTAL_SWING_MODE_TYPE) - ) - if automations_with_entity(hass, entity_id) or scripts_with_entity( - hass, entity_id - ): - async_create_issue( - hass, - DOMAIN, - "deprecated_entity_horizontalswing", - breaks_in_ha_version="2025.8.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_entity_horizontalswing", - translation_placeholders={ - "name": str(entity.name or entity.original_name), - "entity": entity_id, - }, - ) - async_add_entities(entities) - added_devices: set[str] = set() def _add_remove_devices() -> None: diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 4dce104d1c7..1071a7739f6 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -77,22 +77,6 @@ } }, "select": { - "horizontalswing": { - "name": "Horizontal swing", - "state": { - "stopped": "[%key:common::state::off%]", - "fixedleft": "Fixed left", - "fixedcenterleft": "Fixed center left", - "fixedcenter": "Fixed center", - "fixedcenterright": "Fixed center right", - "fixedright": "Fixed right", - "fixedleftright": "Fixed left right", - "rangecenter": "Range center", - "rangefull": "Range full", - "rangeleft": "Range left", - "rangeright": "Range right" - } - }, "light": { "name": "[%key:component::light::title%]", "state": { @@ -153,14 +137,16 @@ "name": "Horizontal swing", "state": { "stopped": "[%key:common::state::off%]", - "fixedleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleft%]", - "fixedcenterleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterleft%]", - "fixedcenter": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenter%]", - "fixedcenterright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterright%]", - "fixedright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedright%]", - "fixedleftright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleftright%]", - "rangecenter": "[%key:component::sensibo::entity::select::horizontalswing::state::rangecenter%]", - "rangefull": "[%key:component::sensibo::entity::select::horizontalswing::state::rangefull%]" + "fixedleft": "Fixed left", + "fixedcenterleft": "Fixed center left", + "fixedcenter": "Fixed center", + "fixedcenterright": "Fixed center right", + "fixedright": "Fixed right", + "fixedleftright": "Fixed left right", + "rangecenter": "Range center", + "rangefull": "Range full", + "rangeleft": "Range left", + "rangeright": "Range right" } }, "light": { @@ -239,14 +225,14 @@ "name": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::name%]", "state": { "stopped": "[%key:common::state::off%]", - "fixedleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleft%]", - "fixedcenterleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterleft%]", - "fixedcenter": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenter%]", - "fixedcenterright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterright%]", - "fixedright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedright%]", - "fixedleftright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleftright%]", - "rangecenter": "[%key:component::sensibo::entity::select::horizontalswing::state::rangecenter%]", - "rangefull": "[%key:component::sensibo::entity::select::horizontalswing::state::rangefull%]" + "fixedleft": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedleft%]", + "fixedcenterleft": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedcenterleft%]", + "fixedcenter": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedcenter%]", + "fixedcenterright": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedcenterright%]", + "fixedright": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedright%]", + "fixedleftright": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedleftright%]", + "rangecenter": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::rangecenter%]", + "rangefull": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::rangefull%]" } }, "light": { @@ -383,7 +369,7 @@ "rangetop": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::rangetop%]", "rangemiddle": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::rangemiddle%]", "rangebottom": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::rangebottom%]", - "rangefull": "[%key:component::sensibo::entity::select::horizontalswing::state::rangefull%]", + "rangefull": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::rangefull%]", "horizontal": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::swing::state::horizontal%]", "both": "[%key:component::climate::entity_component::_::state_attributes::swing_mode::state::both%]" } @@ -391,16 +377,16 @@ "swing_horizontal_mode": { "state": { "stopped": "[%key:common::state::off%]", - "fixedleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleft%]", - "fixedcenterleft": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterleft%]", - "fixedcenter": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenter%]", - "fixedcenterright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedcenterright%]", - "fixedright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedright%]", - "fixedleftright": "[%key:component::sensibo::entity::select::horizontalswing::state::fixedleftright%]", - "rangecenter": "[%key:component::sensibo::entity::select::horizontalswing::state::rangecenter%]", - "rangefull": "[%key:component::sensibo::entity::select::horizontalswing::state::rangefull%]", - "rangeleft": "[%key:component::sensibo::entity::select::horizontalswing::state::rangeleft%]", - "rangeright": "[%key:component::sensibo::entity::select::horizontalswing::state::rangeright%]" + "fixedleft": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedleft%]", + "fixedcenterleft": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedcenterleft%]", + "fixedcenter": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedcenter%]", + "fixedcenterright": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedcenterright%]", + "fixedright": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedright%]", + "fixedleftright": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::fixedleftright%]", + "rangecenter": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::rangecenter%]", + "rangefull": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::rangefull%]", + "rangeleft": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::rangeleft%]", + "rangeright": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::horizontalswing::state::rangeright%]" } } } @@ -590,11 +576,5 @@ "mode_not_exist": { "message": "The entity does not support the chosen mode" } - }, - "issues": { - "deprecated_entity_horizontalswing": { - "title": "The Sensibo {name} entity is deprecated", - "description": "The Sensibo entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts to use the `horizontal_swing` attribute part of the `climate` entity instead.\nDisable `{entity}` and reload the config entry or restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 88f8dbbdaa2..56171707338 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -34,6 +34,7 @@ from homeassistant.util.enum import try_parse_enum from homeassistant.util.hass_dict import HassKey from .const import ( # noqa: F401 + AMBIGUOUS_UNITS, ATTR_LAST_RESET, ATTR_OPTIONS, ATTR_STATE_CLASS, @@ -314,7 +315,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return _numeric_state_expected( try_parse_enum(SensorDeviceClass, self.device_class), self.state_class, - self.native_unit_of_measurement, + self.__native_unit_of_measurement_compat, self.suggested_display_precision, ) @@ -366,7 +367,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): # Make sure we can convert the units if ( (unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None - or self.native_unit_of_measurement not in unit_converter.VALID_UNITS + or self.__native_unit_of_measurement_compat + not in unit_converter.VALID_UNITS or suggested_unit_of_measurement not in unit_converter.VALID_UNITS ): if not self._invalid_suggested_unit_of_measurement_reported: @@ -387,7 +389,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if suggested_unit_of_measurement is None: # Fallback to unit suggested by the unit conversion rules from device class suggested_unit_of_measurement = self.hass.config.units.get_converted_unit( - self.device_class, self.native_unit_of_measurement + self.device_class, self.__native_unit_of_measurement_compat ) if suggested_unit_of_measurement is None and ( @@ -396,7 +398,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): # If the device class is not known by the unit system but has a unit converter, # fall back to the unit suggested by the unit converter's unit class. suggested_unit_of_measurement = self.hass.config.units.get_converted_unit( - unit_converter.UNIT_CLASS, self.native_unit_of_measurement + unit_converter.UNIT_CLASS, self.__native_unit_of_measurement_compat ) if suggested_unit_of_measurement is None: @@ -468,6 +470,16 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self.entity_description.native_unit_of_measurement return None + @final + @property + def __native_unit_of_measurement_compat(self) -> str | None: + """Process ambiguous units.""" + native_unit_of_measurement = self.native_unit_of_measurement + return AMBIGUOUS_UNITS.get( + native_unit_of_measurement, + native_unit_of_measurement, + ) + @cached_property def suggested_unit_of_measurement(self) -> str | None: """Return the unit which should be used for the sensor's state. @@ -503,7 +515,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if self._sensor_option_unit_of_measurement is not UNDEFINED: return self._sensor_option_unit_of_measurement - native_unit_of_measurement = self.native_unit_of_measurement + native_unit_of_measurement = self.__native_unit_of_measurement_compat # Second priority, for non registered entities: unit suggested by integration if not self.registry_entry and ( @@ -543,7 +555,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @override def state(self) -> Any: """Return the state of the sensor and perform unit conversions, if needed.""" - native_unit_of_measurement = self.native_unit_of_measurement + native_unit_of_measurement = self.__native_unit_of_measurement_compat unit_of_measurement = self.unit_of_measurement value = self.native_value # For the sake of validation, we can ignore custom device classes @@ -765,7 +777,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return display_precision default_unit_of_measurement = ( - self.suggested_unit_of_measurement or self.native_unit_of_measurement + self.suggested_unit_of_measurement + or self.__native_unit_of_measurement_compat ) if default_unit_of_measurement is None: return display_precision @@ -843,7 +856,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): (sensor_options := self.registry_entry.options.get(primary_key)) and secondary_key in sensor_options and (device_class := self.device_class) in UNIT_CONVERTERS - and self.native_unit_of_measurement + and self.__native_unit_of_measurement_compat in UNIT_CONVERTERS[device_class].VALID_UNITS and (custom_unit := sensor_options[secondary_key]) in UNIT_CONVERTERS[device_class].VALID_UNITS diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 5f9d5ec9ca0..af35b8127eb 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.const import ( CONCENTRATION_GRAMS_PER_CUBIC_METER, + CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -46,6 +47,7 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.util.unit_conversion import ( + ApparentPowerConverter, AreaConverter, BaseUnitConverter, BloodGlucoseConcentrationConverter, @@ -63,6 +65,7 @@ from homeassistant.util.unit_conversion import ( PowerConverter, PressureConverter, ReactiveEnergyConverter, + ReactivePowerConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -117,7 +120,7 @@ class SensorDeviceClass(StrEnum): APPARENT_POWER = "apparent_power" """Apparent power. - Unit of measurement: `VA` + Unit of measurement: `mVA`, `VA` """ AQI = "aqi" @@ -165,7 +168,7 @@ class SensorDeviceClass(StrEnum): CONDUCTIVITY = "conductivity" """Conductivity. - Unit of measurement: `S/cm`, `mS/cm`, `µS/cm` + Unit of measurement: `S/cm`, `mS/cm`, `μS/cm` """ CURRENT = "current" @@ -197,7 +200,7 @@ class SensorDeviceClass(StrEnum): DURATION = "duration" """Fixed duration. - Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs` + Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs` """ ENERGY = "energy" @@ -277,25 +280,25 @@ class SensorDeviceClass(StrEnum): NITROGEN_DIOXIDE = "nitrogen_dioxide" """Amount of NO2. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ NITROGEN_MONOXIDE = "nitrogen_monoxide" """Amount of NO. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ NITROUS_OXIDE = "nitrous_oxide" """Amount of N2O. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ OZONE = "ozone" """Amount of O3. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ PH = "ph" @@ -307,19 +310,19 @@ class SensorDeviceClass(StrEnum): PM1 = "pm1" """Particulate matter <= 1 μm. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ PM10 = "pm10" """Particulate matter <= 10 μm. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ PM25 = "pm25" """Particulate matter <= 2.5 μm. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ POWER_FACTOR = "power_factor" @@ -369,7 +372,7 @@ class SensorDeviceClass(StrEnum): REACTIVE_POWER = "reactive_power" """Reactive power. - Unit of measurement: `var`, `kvar` + Unit of measurement: `mvar`, `var`, `kvar` """ SIGNAL_STRENGTH = "signal_strength" @@ -397,7 +400,7 @@ class SensorDeviceClass(StrEnum): SULPHUR_DIOXIDE = "sulphur_dioxide" """Amount of SO2. - Unit of measurement: `µg/m³` + Unit of measurement: `μg/m³` """ TEMPERATURE = "temperature" @@ -409,7 +412,7 @@ class SensorDeviceClass(StrEnum): VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. - Unit of measurement: `µg/m³`, `mg/m³` + Unit of measurement: `μg/m³`, `mg/m³` """ VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" @@ -421,7 +424,7 @@ class SensorDeviceClass(StrEnum): VOLTAGE = "voltage" """Voltage. - Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV` + Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV` """ VOLUME = "volume" @@ -449,7 +452,7 @@ class SensorDeviceClass(StrEnum): """Generic flow rate Unit of measurement: UnitOfVolumeFlowRate - - SI / metric: `m³/h`, `L/min`, `mL/s` + - SI / metric: `m³/h`, `m³/min`, `m³/s`, `L/h`, `L/min`, `L/s`, `mL/s` - USCS / imperial: `ft³/min`, `gal/min` """ @@ -468,7 +471,7 @@ class SensorDeviceClass(StrEnum): Weight is used instead of mass to fit with every day language. Unit of measurement: `MASS_*` units - - SI / metric: `µg`, `mg`, `g`, `kg` + - SI / metric: `μg`, `mg`, `g`, `kg` - USCS / imperial: `oz`, `lb` """ @@ -528,6 +531,7 @@ STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass)) STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { + SensorDeviceClass.APPARENT_POWER: ApparentPowerConverter, SensorDeviceClass.ABSOLUTE_HUMIDITY: MassVolumeConcentrationConverter, SensorDeviceClass.AREA: AreaConverter, SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, @@ -548,6 +552,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter, SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter, + SensorDeviceClass.REACTIVE_POWER: ReactivePowerConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: MassVolumeConcentrationConverter, @@ -784,3 +789,16 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]] = { SensorStateClass.MEASUREMENT_ANGLE: {DEGREE}, } + +# We translate units that were using using the legacy coding of μ \u00b5 +# to units using recommended coding of μ \u03bc +AMBIGUOUS_UNITS: dict[str | None, str] = { + "\u00b5Sv/h": "μSv/h", # aranet: radiation rate + "\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM, + "\u00b5V": UnitOfElectricPotential.MICROVOLT, + "\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, + "\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light + "\u00b5g": UnitOfMass.MICROGRAMS, + "\u00b5s": UnitOfTime.MICROSECONDS, +} diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index c321caa616d..c20a3e2e1ae 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -45,6 +45,7 @@ from homeassistant.util.enum import try_parse_enum from homeassistant.util.hass_dict import HassKey from .const import ( + AMBIGUOUS_UNITS, ATTR_LAST_RESET, ATTR_STATE_CLASS, DOMAIN, @@ -79,7 +80,7 @@ EQUIVALENT_UNITS = { "ft3": UnitOfVolume.CUBIC_FEET, "m3": UnitOfVolume.CUBIC_METERS, "ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, -} +} | AMBIGUOUS_UNITS # Keep track of entities for which a warning about decreasing value has been logged diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index c69bf99eff0..a8d06f8c0e9 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -25,7 +25,7 @@ "is_illuminance": "Current {entity_name} illuminance", "is_irradiance": "Current {entity_name} irradiance", "is_moisture": "Current {entity_name} moisture", - "is_monetary": "Current {entity_name} balance", + "is_monetary": "Current {entity_name} monetary balance", "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", @@ -81,7 +81,7 @@ "illuminance": "{entity_name} illuminance changes", "irradiance": "{entity_name} irradiance changes", "moisture": "{entity_name} moisture changes", - "monetary": "{entity_name} balance changes", + "monetary": "{entity_name} monetary balance changes", "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", @@ -223,7 +223,7 @@ "name": "Moisture" }, "monetary": { - "name": "Balance" + "name": "Monetary balance" }, "nitrogen_dioxide": { "name": "Nitrogen dioxide" diff --git a/homeassistant/components/sleep_as_android/__init__.py b/homeassistant/components/sleep_as_android/__init__.py new file mode 100644 index 00000000000..8dd08ba0388 --- /dev/null +++ b/homeassistant/components/sleep_as_android/__init__.py @@ -0,0 +1,66 @@ +"""The Sleep as Android integration.""" + +from __future__ import annotations + +from http import HTTPStatus + +from aiohttp.web import Request, Response +import voluptuous as vol + +from homeassistant.components import webhook +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_WEBHOOK_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ATTR_EVENT, ATTR_VALUE1, ATTR_VALUE2, ATTR_VALUE3, DOMAIN + +PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR] + +type SleepAsAndroidConfigEntry = ConfigEntry + +WEBHOOK_SCHEMA = vol.Schema( + { + vol.Required(ATTR_EVENT): str, + vol.Optional(ATTR_VALUE1): str, + vol.Optional(ATTR_VALUE2): str, + vol.Optional(ATTR_VALUE3): str, + } +) + + +async def handle_webhook( + hass: HomeAssistant, webhook_id: str, request: Request +) -> Response: + """Handle incoming Sleep as Android webhook request.""" + + try: + data = WEBHOOK_SCHEMA(await request.json()) + except vol.MultipleInvalid as error: + return Response( + text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + ) + + async_dispatcher_send(hass, DOMAIN, webhook_id, data) + return Response(status=HTTPStatus.NO_CONTENT) + + +async def async_setup_entry( + hass: HomeAssistant, entry: SleepAsAndroidConfigEntry +) -> bool: + """Set up Sleep as Android from a config entry.""" + + webhook.async_register( + hass, DOMAIN, entry.title, entry.data[CONF_WEBHOOK_ID], handle_webhook + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry( + hass: HomeAssistant, entry: SleepAsAndroidConfigEntry +) -> bool: + """Unload a config entry.""" + webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sleep_as_android/config_flow.py b/homeassistant/components/sleep_as_android/config_flow.py new file mode 100644 index 00000000000..595612cc601 --- /dev/null +++ b/homeassistant/components/sleep_as_android/config_flow.py @@ -0,0 +1,14 @@ +"""Config flow for the Sleep as Android integration.""" + +from __future__ import annotations + +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + +config_entry_flow.register_webhook_flow( + DOMAIN, + "Sleep as Android", + {"docs_url": "https://www.home-assistant.io/integrations/sleep_as_android"}, + allow_multiple=True, +) diff --git a/homeassistant/components/sleep_as_android/const.py b/homeassistant/components/sleep_as_android/const.py new file mode 100644 index 00000000000..37cf3f14261 --- /dev/null +++ b/homeassistant/components/sleep_as_android/const.py @@ -0,0 +1,32 @@ +"""Constants for the Sleep as Android integration.""" + +DOMAIN = "sleep_as_android" + +ATTR_EVENT = "event" +ATTR_VALUE1 = "value1" +ATTR_VALUE2 = "value2" +ATTR_VALUE3 = "value3" + +MAP_EVENTS = { + "sleep_tracking_paused": "paused", + "sleep_tracking_resumed": "resumed", + "sleep_tracking_started": "started", + "sleep_tracking_stopped": "stopped", + "alarm_alert_dismiss": "alert_dismiss", + "alarm_alert_start": "alert_start", + "alarm_rescheduled": "rescheduled", + "alarm_skip_next": "skip_next", + "alarm_snooze_canceled": "snooze_canceled", + "alarm_snooze_clicked": "snooze_clicked", + "alarm_wake_up_check": "wake_up_check", + "sound_event_baby": "baby", + "sound_event_cough": "cough", + "sound_event_laugh": "laugh", + "sound_event_snore": "snore", + "sound_event_talk": "talk", + "lullaby_start": "start", + "lullaby_stop": "stop", + "lullaby_volume_down": "volume_down", +} + +ALARM_LABEL_DEFAULT = "alarm" diff --git a/homeassistant/components/sleep_as_android/diagnostics.py b/homeassistant/components/sleep_as_android/diagnostics.py new file mode 100644 index 00000000000..2f49e818ece --- /dev/null +++ b/homeassistant/components/sleep_as_android/diagnostics.py @@ -0,0 +1,19 @@ +"""Diagnostics platform for Sleep as Android integration.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant + +from . import SleepAsAndroidConfigEntry + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: SleepAsAndroidConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + return { + "config_entry_data": {"cloudhook": config_entry.data["cloudhook"]}, + } diff --git a/homeassistant/components/sleep_as_android/entity.py b/homeassistant/components/sleep_as_android/entity.py new file mode 100644 index 00000000000..5984bb45efd --- /dev/null +++ b/homeassistant/components/sleep_as_android/entity.py @@ -0,0 +1,47 @@ +"""Base entity for Sleep as Android integration.""" + +from __future__ import annotations + +from abc import abstractmethod + +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity, EntityDescription + +from . import SleepAsAndroidConfigEntry +from .const import DOMAIN + + +class SleepAsAndroidEntity(Entity): + """Base entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + config_entry: SleepAsAndroidConfigEntry, + entity_description: EntityDescription, + ) -> None: + """Initialize the entity.""" + + self._attr_unique_id = f"{config_entry.entry_id}_{entity_description.key}" + self.entity_description = entity_description + self.webhook_id = config_entry.data[CONF_WEBHOOK_ID] + self._attr_device_info = DeviceInfo( + connections={(DOMAIN, config_entry.entry_id)}, + manufacturer="Urbandroid", + model="Sleep as Android", + name=config_entry.title, + ) + + @abstractmethod + def _async_handle_event(self, webhook_id: str, data: dict[str, str]) -> None: + """Handle the Sleep as Android event.""" + + async def async_added_to_hass(self) -> None: + """Register event callback.""" + + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self._async_handle_event) + ) diff --git a/homeassistant/components/sleep_as_android/event.py b/homeassistant/components/sleep_as_android/event.py new file mode 100644 index 00000000000..20a3690a0a5 --- /dev/null +++ b/homeassistant/components/sleep_as_android/event.py @@ -0,0 +1,153 @@ +"""Event platform for Sleep as Android integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import SleepAsAndroidConfigEntry +from .const import ATTR_EVENT, MAP_EVENTS +from .entity import SleepAsAndroidEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(kw_only=True, frozen=True) +class SleepAsAndroidEventEntityDescription(EventEntityDescription): + """Sleep as Android sensor description.""" + + event_types: list[str] + + +class SleepAsAndroidEvent(StrEnum): + """Sleep as Android events.""" + + ALARM_CLOCK = "alarm_clock" + USER_NOTIFICATION = "user_notification" + SMART_WAKEUP = "smart_wakeup" + SLEEP_HEALTH = "sleep_health" + LULLABY = "lullaby" + SLEEP_PHASE = "sleep_phase" + SLEEP_TRACKING = "sleep_tracking" + SOUND_EVENT = "sound_event" + + +EVENT_DESCRIPTIONS: tuple[SleepAsAndroidEventEntityDescription, ...] = ( + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.SLEEP_TRACKING, + translation_key=SleepAsAndroidEvent.SLEEP_TRACKING, + device_class=EventDeviceClass.BUTTON, + event_types=[ + "paused", + "resumed", + "started", + "stopped", + ], + ), + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.ALARM_CLOCK, + translation_key=SleepAsAndroidEvent.ALARM_CLOCK, + event_types=[ + "alert_dismiss", + "alert_start", + "rescheduled", + "skip_next", + "snooze_canceled", + "snooze_clicked", + ], + ), + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.SMART_WAKEUP, + translation_key=SleepAsAndroidEvent.SMART_WAKEUP, + event_types=[ + "before_smart_period", + "smart_period", + ], + ), + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.USER_NOTIFICATION, + translation_key=SleepAsAndroidEvent.USER_NOTIFICATION, + event_types=[ + "wake_up_check", + "show_skip_next_alarm", + "time_to_bed_alarm_alert", + ], + ), + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.SLEEP_PHASE, + translation_key=SleepAsAndroidEvent.SLEEP_PHASE, + event_types=[ + "awake", + "deep_sleep", + "light_sleep", + "not_awake", + "rem", + ], + ), + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.SOUND_EVENT, + translation_key=SleepAsAndroidEvent.SOUND_EVENT, + event_types=[ + "baby", + "cough", + "laugh", + "snore", + "talk", + ], + ), + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.LULLABY, + translation_key=SleepAsAndroidEvent.LULLABY, + event_types=[ + "start", + "stop", + "volume_down", + ], + ), + SleepAsAndroidEventEntityDescription( + key=SleepAsAndroidEvent.SLEEP_HEALTH, + translation_key=SleepAsAndroidEvent.SLEEP_HEALTH, + event_types=[ + "antisnoring", + "apnea_alarm", + ], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: SleepAsAndroidConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the event platform.""" + + async_add_entities( + SleepAsAndroidEventEntity(config_entry, description) + for description in EVENT_DESCRIPTIONS + ) + + +class SleepAsAndroidEventEntity(SleepAsAndroidEntity, EventEntity): + """An event entity.""" + + entity_description: SleepAsAndroidEventEntityDescription + + @callback + def _async_handle_event(self, webhook_id: str, data: dict[str, str]) -> None: + """Handle the Sleep as Android event.""" + event = MAP_EVENTS.get(data[ATTR_EVENT], data[ATTR_EVENT]) + if ( + webhook_id == self.webhook_id + and event in self.entity_description.event_types + ): + self._trigger_event(event) + self.async_write_ha_state() diff --git a/homeassistant/components/sleep_as_android/icons.json b/homeassistant/components/sleep_as_android/icons.json new file mode 100644 index 00000000000..0565716a5f1 --- /dev/null +++ b/homeassistant/components/sleep_as_android/icons.json @@ -0,0 +1,38 @@ +{ + "entity": { + "event": { + "alarm_clock": { + "default": "mdi:alarm" + }, + "user_notification": { + "default": "mdi:cellphone-message" + }, + "smart_wakeup": { + "default": "mdi:brain" + }, + "sleep_phase": { + "default": "mdi:bed" + }, + "sound_event": { + "default": "mdi:chat-sleep-outline" + }, + "sleep_tracking": { + "default": "mdi:record-rec" + }, + "lullaby": { + "default": "mdi:cradle-outline" + }, + "sleep_health": { + "default": "mdi:heart-pulse" + } + }, + "sensor": { + "alarm_time": { + "default": "mdi:alarm" + }, + "alarm_label": { + "default": "mdi:label-outline" + } + } + } +} diff --git a/homeassistant/components/sleep_as_android/manifest.json b/homeassistant/components/sleep_as_android/manifest.json new file mode 100644 index 00000000000..fbac134ffa1 --- /dev/null +++ b/homeassistant/components/sleep_as_android/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "sleep_as_android", + "name": "Sleep as Android", + "codeowners": ["@tr4nt0r"], + "config_flow": true, + "dependencies": ["webhook"], + "documentation": "https://www.home-assistant.io/integrations/sleep_as_android", + "iot_class": "local_push", + "quality_scale": "silver" +} diff --git a/homeassistant/components/sleep_as_android/quality_scale.yaml b/homeassistant/components/sleep_as_android/quality_scale.yaml new file mode 100644 index 00000000000..acc2d8d11f0 --- /dev/null +++ b/homeassistant/components/sleep_as_android/quality_scale.yaml @@ -0,0 +1,110 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: has no actions + appropriate-polling: + status: exempt + comment: does not poll + brands: done + common-modules: done + config-flow-test-coverage: + status: done + comment: uses webhook flow helper, already covered + config-flow: done + dependency-transparency: + status: exempt + comment: no dependencies + docs-actions: + status: exempt + comment: no actions + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: + status: exempt + comment: has no runtime data + test-before-configure: + status: exempt + comment: nothing to test + test-before-setup: + status: exempt + comment: nothing to test + unique-config-entry: + status: exempt + comment: only 1 webhook can be configured per device. It's not possible to prevent different devices from using the same webhook + + # Silver + action-exceptions: + status: exempt + comment: no actions + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: integration has no options + docs-installation-parameters: done + entity-unavailable: + status: exempt + comment: only state-less entities + integration-owner: done + log-when-unavailable: + status: exempt + comment: only state-less entities + parallel-updates: done + reauthentication-flow: + status: exempt + comment: no authentication required + test-coverage: done + + # Gold + devices: done + diagnostics: done + discovery-update-info: + status: exempt + comment: no discovery + discovery: + status: exempt + comment: cannot be discovered + docs-data-update: + status: exempt + comment: does not poll + docs-examples: done + docs-known-limitations: done + docs-supported-devices: + status: exempt + comment: has no devices + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done + dynamic-devices: + status: exempt + comment: has no devices + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: + status: exempt + comment: does not raise exceptions + icon-translations: done + reconfiguration-flow: + status: exempt + comment: webhook config flow helper does not implement reconfigure + repair-issues: + status: exempt + comment: has no repairs + stale-devices: + status: exempt + comment: has no stale devices + + # Platinum + async-dependency: + status: exempt + comment: has no external dependencies + inject-websession: + status: exempt + comment: does not do http requests + strict-typing: done diff --git a/homeassistant/components/sleep_as_android/sensor.py b/homeassistant/components/sleep_as_android/sensor.py new file mode 100644 index 00000000000..966e851f633 --- /dev/null +++ b/homeassistant/components/sleep_as_android/sensor.py @@ -0,0 +1,96 @@ +"""Sensor platform for Sleep as Android integration.""" + +from __future__ import annotations + +from datetime import datetime +from enum import StrEnum + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util import dt as dt_util + +from . import SleepAsAndroidConfigEntry +from .const import ALARM_LABEL_DEFAULT, ATTR_EVENT, ATTR_VALUE1, ATTR_VALUE2 +from .entity import SleepAsAndroidEntity + +PARALLEL_UPDATES = 0 + + +class SleepAsAndroidSensor(StrEnum): + """Sleep as Android sensors.""" + + NEXT_ALARM = "next_alarm" + ALARM_LABEL = "alarm_label" + + +SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=SleepAsAndroidSensor.NEXT_ALARM, + translation_key=SleepAsAndroidSensor.NEXT_ALARM, + device_class=SensorDeviceClass.TIMESTAMP, + ), + SensorEntityDescription( + key=SleepAsAndroidSensor.ALARM_LABEL, + translation_key=SleepAsAndroidSensor.ALARM_LABEL, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: SleepAsAndroidConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + + async_add_entities( + SleepAsAndroidSensorEntity(config_entry, description) + for description in SENSOR_DESCRIPTIONS + ) + + +class SleepAsAndroidSensorEntity(SleepAsAndroidEntity, RestoreSensor): + """A sensor entity.""" + + entity_description: SensorEntityDescription + + @callback + def _async_handle_event(self, webhook_id: str, data: dict[str, str]) -> None: + """Handle the Sleep as Android event.""" + + if webhook_id == self.webhook_id and data[ATTR_EVENT] in ( + "alarm_snooze_clicked", + "alarm_snooze_canceled", + "alarm_alert_start", + "alarm_alert_dismiss", + "alarm_skip_next", + "show_skip_next_alarm", + "alarm_rescheduled", + ): + if ( + self.entity_description.key is SleepAsAndroidSensor.NEXT_ALARM + and (alarm_time := data.get(ATTR_VALUE1)) + and alarm_time.isnumeric() + ): + self._attr_native_value = datetime.fromtimestamp( + int(alarm_time) / 1000, tz=dt_util.get_default_time_zone() + ) + if self.entity_description.key is SleepAsAndroidSensor.ALARM_LABEL and ( + label := data.get(ATTR_VALUE2, ALARM_LABEL_DEFAULT) + ): + self._attr_native_value = label + + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Restore entity state.""" + state = await self.async_get_last_sensor_data() + if state: + self._attr_native_value = state.native_value + + await super().async_added_to_hass() diff --git a/homeassistant/components/sleep_as_android/strings.json b/homeassistant/components/sleep_as_android/strings.json new file mode 100644 index 00000000000..f36b26e5b58 --- /dev/null +++ b/homeassistant/components/sleep_as_android/strings.json @@ -0,0 +1,134 @@ +{ + "config": { + "step": { + "user": { + "title": "Set up Sleep as Android", + "description": "Are you sure you want to set up the Sleep as Android integration?" + } + }, + "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", + "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to set up a webhook.\n\nOpen Sleep as Android and go to *Settings → Services → Automation → Webhooks*\n\nEnable *Webhooks* and fill in the following webhook in the URL field:\n\n`{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + } + }, + "entity": { + "event": { + "sleep_tracking": { + "name": "Sleep tracking", + "state_attributes": { + "event_type": { + "state": { + "paused": "[%key:common::state::paused%]", + "resumed": "Resumed", + "started": "Started", + "stopped": "[%key:common::state::stopped%]" + } + } + } + }, + "alarm_clock": { + "name": "Alarm clock", + "state_attributes": { + "event_type": { + "state": { + "alert_dismiss": "Alarm dismissed", + "alert_start": "Alarm started", + "rescheduled": "Alarm rescheduled", + "skip_next": "Alarm skipped", + "snooze_canceled": "Snooze canceled", + "snooze_clicked": "Snoozing" + } + } + } + }, + "smart_wakeup": { + "name": "Smart wake-up", + "state_attributes": { + "event_type": { + "state": { + "before_smart_period": "45min before smart wake-up", + "smart_period": "Smart wake-up started" + } + } + } + }, + "user_notification": { + "name": "User notification", + "state_attributes": { + "event_type": { + "state": { + "wake_up_check": "Wake-up check", + "show_skip_next_alarm": "Skip next alarm", + "time_to_bed_alarm_alert": "Time to bed" + } + } + } + }, + "sleep_phase": { + "name": "Sleep phase", + "state_attributes": { + "event_type": { + "state": { + "awake": "Woke up", + "deep_sleep": "Deep sleep", + "light_sleep": "Light sleep", + "not_awake": "Fell asleep", + "rem": "REM sleep" + } + } + } + }, + "sound_event": { + "name": "Sound recognition", + "state_attributes": { + "event_type": { + "state": { + "baby": "Baby crying", + "cough": "Coughing or sneezing", + "laugh": "Laughter", + "snore": "Snoring", + "talk": "Talking" + } + } + } + }, + "lullaby": { + "name": "Lullaby", + "state_attributes": { + "event_type": { + "state": { + "start": "Started", + "stop": "[%key:common::state::stopped%]", + "volume_down": "Lowering volume" + } + } + } + }, + "sleep_health": { + "name": "Sleep health", + "state_attributes": { + "event_type": { + "state": { + "antisnoring": "Anti-snoring triggered", + "apnea_alarm": "Sleep apnea detected" + } + } + } + } + }, + "sensor": { + "next_alarm": { + "name": "Next alarm" + }, + "alarm_label": { + "name": "Alarm label", + "state": { + "alarm": "Alarm" + } + } + } + } +} diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 5082e2313df..dd2e05ee3ba 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/sleepiq", "iot_class": "cloud_polling", "loggers": ["asyncsleepiq"], - "requirements": ["asyncsleepiq==1.5.3"] + "requirements": ["asyncsleepiq==1.6.0"] } diff --git a/homeassistant/components/smarla/const.py b/homeassistant/components/smarla/const.py index f81ccd328bc..fcb64f1e315 100644 --- a/homeassistant/components/smarla/const.py +++ b/homeassistant/components/smarla/const.py @@ -6,7 +6,7 @@ DOMAIN = "smarla" HOST = "https://devices.swing2sleep.de" -PLATFORMS = [Platform.NUMBER, Platform.SWITCH] +PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] DEVICE_MODEL_NAME = "Smarla" MANUFACTURER_NAME = "Swing2Sleep" diff --git a/homeassistant/components/smarla/icons.json b/homeassistant/components/smarla/icons.json index 2ba7404cc35..a72e7e7ea12 100644 --- a/homeassistant/components/smarla/icons.json +++ b/homeassistant/components/smarla/icons.json @@ -9,6 +9,20 @@ "intensity": { "default": "mdi:sine-wave" } + }, + "sensor": { + "amplitude": { + "default": "mdi:sine-wave" + }, + "period": { + "default": "mdi:sine-wave" + }, + "activity": { + "default": "mdi:baby-face" + }, + "swing_count": { + "default": "mdi:counter" + } } } } diff --git a/homeassistant/components/smarla/manifest.json b/homeassistant/components/smarla/manifest.json index e2e9e08dcab..a99cf9b4891 100644 --- a/homeassistant/components/smarla/manifest.json +++ b/homeassistant/components/smarla/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["pysmarlaapi", "pysignalr"], "quality_scale": "bronze", - "requirements": ["pysmarlaapi==0.9.1"] + "requirements": ["pysmarlaapi==0.9.2"] } diff --git a/homeassistant/components/smarla/sensor.py b/homeassistant/components/smarla/sensor.py new file mode 100644 index 00000000000..18bef76e320 --- /dev/null +++ b/homeassistant/components/smarla/sensor.py @@ -0,0 +1,107 @@ +"""Support for the Swing2Sleep Smarla sensor entities.""" + +from dataclasses import dataclass + +from pysmarlaapi.federwiege.classes import Property + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import UnitOfLength, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import FederwiegeConfigEntry +from .entity import SmarlaBaseEntity, SmarlaEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class SmarlaSensorEntityDescription(SmarlaEntityDescription, SensorEntityDescription): + """Class describing Swing2Sleep Smarla sensor entities.""" + + multiple: bool = False + value_pos: int = 0 + + +SENSORS: list[SmarlaSensorEntityDescription] = [ + SmarlaSensorEntityDescription( + key="amplitude", + translation_key="amplitude", + service="analyser", + property="oscillation", + multiple=True, + value_pos=0, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + SmarlaSensorEntityDescription( + key="period", + translation_key="period", + service="analyser", + property="oscillation", + multiple=True, + value_pos=1, + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + ), + SmarlaSensorEntityDescription( + key="activity", + translation_key="activity", + service="analyser", + property="activity", + state_class=SensorStateClass.MEASUREMENT, + ), + SmarlaSensorEntityDescription( + key="swing_count", + translation_key="swing_count", + service="analyser", + property="swing_count", + state_class=SensorStateClass.TOTAL_INCREASING, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: FederwiegeConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Smarla sensors from config entry.""" + federwiege = config_entry.runtime_data + async_add_entities( + ( + SmarlaSensor(federwiege, desc) + if not desc.multiple + else SmarlaSensorMultiple(federwiege, desc) + ) + for desc in SENSORS + ) + + +class SmarlaSensor(SmarlaBaseEntity, SensorEntity): + """Representation of Smarla sensor.""" + + entity_description: SmarlaSensorEntityDescription + + _property: Property[int] + + @property + def native_value(self) -> int | None: + """Return the entity value to represent the entity state.""" + return self._property.get() + + +class SmarlaSensorMultiple(SmarlaBaseEntity, SensorEntity): + """Representation of Smarla sensor with multiple values inside property.""" + + entity_description: SmarlaSensorEntityDescription + + _property: Property[list[int]] + + @property + def native_value(self) -> int | None: + """Return the entity value to represent the entity state.""" + v = self._property.get() + return v[self.entity_description.value_pos] if v is not None else None diff --git a/homeassistant/components/smarla/strings.json b/homeassistant/components/smarla/strings.json index fbe5df4c1d0..edf306b1183 100644 --- a/homeassistant/components/smarla/strings.json +++ b/homeassistant/components/smarla/strings.json @@ -28,6 +28,21 @@ "intensity": { "name": "Intensity" } + }, + "sensor": { + "amplitude": { + "name": "Amplitude" + }, + "period": { + "name": "Period" + }, + "activity": { + "name": "Activity" + }, + "swing_count": { + "name": "Swing count", + "unit_of_measurement": "swings" + } } } } diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 35354570f23..951d1372a69 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.8"] + "requirements": ["pysmartthings==3.2.9"] } diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index a38331d6aed..d3e2ab09a3f 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -476,6 +476,16 @@ CAPABILITY_TO_SENSORS: dict[ ) ] }, + Capability.FINE_DUST_SENSOR: { + Attribute.FINE_DUST_LEVEL: [ + SmartThingsSensorEntityDescription( + key=Attribute.FINE_DUST_LEVEL, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.PM25, + ) + ] + }, # Haven't seen at devices yet Capability.FORMALDEHYDE_MEASUREMENT: { Attribute.FORMALDEHYDE_LEVEL: [ diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json index 9340573f6ce..32037b51ede 100644 --- a/homeassistant/components/smlight/manifest.json +++ b/homeassistant/components/smlight/manifest.json @@ -12,7 +12,7 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "silver", - "requirements": ["pysmlight==0.2.7"], + "requirements": ["pysmlight==0.2.8"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/homeassistant/components/snapcast/coordinator.py b/homeassistant/components/snapcast/coordinator.py index 4c2f0cb81b7..963f12887fc 100644 --- a/homeassistant/components/snapcast/coordinator.py +++ b/homeassistant/components/snapcast/coordinator.py @@ -39,6 +39,8 @@ class SnapcastUpdateCoordinator(DataUpdateCoordinator[None]): self._server.set_on_connect_callback(self._on_connect) self._server.set_on_disconnect_callback(self._on_disconnect) + self._host_id = f"{host}:{port}" + def _on_update(self) -> None: """Snapserver on_update callback.""" # Assume availability if an update is received. @@ -77,3 +79,8 @@ class SnapcastUpdateCoordinator(DataUpdateCoordinator[None]): def server(self) -> Snapserver: """Get the Snapserver object.""" return self._server + + @property + def host_id(self) -> str: + """Get the host ID.""" + return self._host_id diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 8e3f787e71d..ccb9d4c4c46 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping import logging from typing import Any @@ -19,13 +19,13 @@ from homeassistant.components.media_player import ( MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import ( config_validation as cv, entity_platform, entity_registry as er, + issue_registry as ir, ) from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -52,6 +52,12 @@ STREAM_STATUS = { "unknown": None, } +_SUPPORTED_FEATURES = ( + MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.SELECT_SOURCE +) + _LOGGER = logging.getLogger(__name__) @@ -82,106 +88,91 @@ async def async_setup_entry( # Fetch coordinator from global data coordinator: SnapcastUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # Create an ID for the Snapserver - host = config_entry.data[CONF_HOST] - port = config_entry.data[CONF_PORT] - host_id = f"{host}:{port}" - register_services() _known_group_ids: set[str] = set() _known_client_ids: set[str] = set() @callback - def _check_entities() -> None: - nonlocal _known_group_ids, _known_client_ids + def _update_entities( + entity_class: type[SnapcastClientDevice | SnapcastGroupDevice], + known_ids: set[str], + get_device: Callable[[str], Snapclient | Snapgroup], + get_devices: Callable[[], list[Snapclient] | list[Snapgroup]], + ) -> None: + # Get IDs of current devices on server + snapcast_ids = {d.identifier for d in get_devices()} - def _update_known_ids(known_ids, ids) -> tuple[set[str], set[str]]: - ids_to_add = ids - known_ids - ids_to_remove = known_ids - ids + # Update known IDs + ids_to_add = snapcast_ids - known_ids + ids_to_remove = known_ids - snapcast_ids - # Update known IDs - known_ids.difference_update(ids_to_remove) - known_ids.update(ids_to_add) - - return ids_to_add, ids_to_remove - - group_ids = {g.identifier for g in coordinator.server.groups} - groups_to_add, groups_to_remove = _update_known_ids(_known_group_ids, group_ids) - - client_ids = {c.identifier for c in coordinator.server.clients} - clients_to_add, clients_to_remove = _update_known_ids( - _known_client_ids, client_ids - ) + known_ids.difference_update(ids_to_remove) + known_ids.update(ids_to_add) # Exit early if no changes - if not (groups_to_add | groups_to_remove | clients_to_add | clients_to_remove): + if not (ids_to_add | ids_to_remove): return _LOGGER.debug( - "New clients: %s", - str([coordinator.server.client(c).friendly_name for c in clients_to_add]), + "New %s: %s", + entity_class, + str([get_device(d).friendly_name for d in ids_to_add]), ) _LOGGER.debug( - "New groups: %s", - str([coordinator.server.group(g).friendly_name for g in groups_to_add]), - ) - _LOGGER.debug( - "Remove client IDs: %s", - str([list(clients_to_remove)]), - ) - _LOGGER.debug( - "Remove group IDs: %s", - str(list(groups_to_remove)), + "Remove %s IDs: %s", + entity_class, + str([list(ids_to_remove)]), ) # Add new entities async_add_entities( [ - SnapcastGroupDevice( - coordinator, coordinator.server.group(group_id), host_id - ) - for group_id in groups_to_add - ] - + [ - SnapcastClientDevice( - coordinator, coordinator.server.client(client_id), host_id - ) - for client_id in clients_to_add + entity_class(coordinator, get_device(snapcast_id)) + for snapcast_id in ids_to_add ] ) # Remove stale entities entity_registry = er.async_get(hass) - for group_id in groups_to_remove: + for snapcast_id in ids_to_remove: if entity_id := entity_registry.async_get_entity_id( MEDIA_PLAYER_DOMAIN, DOMAIN, - SnapcastGroupDevice.get_unique_id(host_id, group_id), + entity_class.get_unique_id(coordinator.host_id, snapcast_id), ): entity_registry.async_remove(entity_id) - for client_id in clients_to_remove: - if entity_id := entity_registry.async_get_entity_id( - MEDIA_PLAYER_DOMAIN, - DOMAIN, - SnapcastClientDevice.get_unique_id(host_id, client_id), - ): - entity_registry.async_remove(entity_id) + def _update_clients() -> None: + _update_entities( + SnapcastClientDevice, + _known_client_ids, + coordinator.server.client, + lambda: coordinator.server.clients, + ) - coordinator.async_add_listener(_check_entities) - _check_entities() + # Create client entities and add listener to update clients on server update + _update_clients() + coordinator.async_add_listener(_update_clients) + + def _update_groups() -> None: + _update_entities( + SnapcastGroupDevice, + _known_group_ids, + coordinator.server.group, + lambda: coordinator.server.groups, + ) + + # Create group entities and add listener to update groups on server update + _update_groups() + coordinator.async_add_listener(_update_groups) class SnapcastBaseDevice(SnapcastCoordinatorEntity, MediaPlayerEntity): """Base class representing a Snapcast device.""" _attr_should_poll = False - _attr_supported_features = ( - MediaPlayerEntityFeature.VOLUME_MUTE - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.SELECT_SOURCE - ) + _attr_supported_features = _SUPPORTED_FEATURES _attr_media_content_type = MediaType.MUSIC _attr_device_class = MediaPlayerDeviceClass.SPEAKER @@ -189,13 +180,14 @@ class SnapcastBaseDevice(SnapcastCoordinatorEntity, MediaPlayerEntity): self, coordinator: SnapcastUpdateCoordinator, device: Snapgroup | Snapclient, - host_id: str, ) -> None: """Initialize the base device.""" super().__init__(coordinator) self._device = device - self._attr_unique_id = self.get_unique_id(host_id, device.identifier) + self._attr_unique_id = self.get_unique_id( + coordinator.host_id, device.identifier + ) @classmethod def get_unique_id(cls, host, id) -> str: @@ -279,6 +271,19 @@ class SnapcastBaseDevice(SnapcastCoordinatorEntity, MediaPlayerEntity): """Handle the unjoin service.""" raise NotImplementedError + def _async_create_grouping_deprecation_issue(self) -> None: + """Create an issue for deprecated grouping actions.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "deprecated_grouping_actions", + breaks_in_ha_version="2026.2.0", + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_grouping_actions", + ) + @property def metadata(self) -> Mapping[str, Any]: """Get metadata from the current stream.""" @@ -389,11 +394,62 @@ class SnapcastGroupDevice(SnapcastBaseDevice): """Handle the unjoin service.""" raise ServiceValidationError("Entity is not a client. Can only unjoin clients.") + def _async_create_group_deprecation_issue(self) -> None: + """Create an issue for deprecated group entities.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "deprecated_group_entities", + breaks_in_ha_version="2026.2.0", + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_group_entities", + ) + + async def async_select_source(self, source: str) -> None: + """Set input source.""" + # Groups are deprecated, create an issue when used + self._async_create_group_deprecation_issue() + + await super().async_select_source(source) + + async def async_mute_volume(self, mute: bool) -> None: + """Send the mute command.""" + # Groups are deprecated, create an issue when used + self._async_create_group_deprecation_issue() + + await super().async_mute_volume(mute) + + async def async_set_volume_level(self, volume: float) -> None: + """Set the volume level.""" + # Groups are deprecated, create an issue when used + self._async_create_group_deprecation_issue() + + await super().async_set_volume_level(volume) + + def snapshot(self) -> None: + """Snapshot the group state.""" + # Groups are deprecated, create an issue when used + self._async_create_group_deprecation_issue() + + super().snapshot() + + async def async_restore(self) -> None: + """Restore the group state.""" + # Groups are deprecated, create an issue when used + self._async_create_group_deprecation_issue() + + await super().async_restore() + class SnapcastClientDevice(SnapcastBaseDevice): """Representation of a Snapcast client device.""" _device: Snapclient + _attr_supported_features = ( + _SUPPORTED_FEATURES | MediaPlayerEntityFeature.GROUPING + ) # Clients support grouping @classmethod def get_unique_id(cls, host, id) -> str: @@ -439,6 +495,9 @@ class SnapcastClientDevice(SnapcastBaseDevice): async def async_join(self, master) -> None: """Join the group of the master player.""" + # Action is deprecated, create an issue + self._async_create_grouping_deprecation_issue() + entity_registry = er.async_get(self.hass) master_entity = entity_registry.async_get(master) if master_entity is None: @@ -463,5 +522,53 @@ class SnapcastClientDevice(SnapcastBaseDevice): async def async_unjoin(self) -> None: """Unjoin the group the player is currently in.""" + # Action is deprecated, create an issue + self._async_create_grouping_deprecation_issue() + + await self._current_group.remove_client(self._device.identifier) + self.async_write_ha_state() + + @property + def group_members(self) -> list[str] | None: + """List of player entities which are currently grouped together for synchronous playback.""" + entity_registry = er.async_get(self.hass) + return [ + entity_id + for client_id in self._current_group.clients + if ( + entity_id := entity_registry.async_get_entity_id( + MEDIA_PLAYER_DOMAIN, + DOMAIN, + self.get_unique_id(self.coordinator.host_id, client_id), + ) + ) + ] + + async def async_join_players(self, group_members: list[str]) -> None: + """Add `group_members` to this client's current group.""" + # Get the client entity for each group member excluding self + entity_registry = er.async_get(self.hass) + clients = [ + entity + for entity_id in group_members + if (entity := entity_registry.async_get(entity_id)) + and entity.unique_id != self.unique_id + ] + + for client in clients: + # Valid entity is a snapcast client + if not client.unique_id.startswith(CLIENT_PREFIX): + raise ServiceValidationError( + f"Entity '{client.entity_id}' is not a Snapcast client device." + ) + + # Extract client ID and join it to the current group + identifier = client.unique_id.split("_")[-1] + await self._current_group.add_client(identifier) + + self.async_write_ha_state() + + async def async_unjoin_player(self) -> None: + """Remove this client from it's current group.""" await self._current_group.remove_client(self._device.identifier) self.async_write_ha_state() diff --git a/homeassistant/components/snapcast/strings.json b/homeassistant/components/snapcast/strings.json index 685b4a0dd11..9336b1fac86 100644 --- a/homeassistant/components/snapcast/strings.json +++ b/homeassistant/components/snapcast/strings.json @@ -58,5 +58,15 @@ } } } + }, + "issues": { + "deprecated_grouping_actions": { + "title": "Snapcast Actions Deprecated", + "description": "Actions 'snapcast.join' and 'snapcast.unjoin' are deprecated and will be removed in 2026.2. Use the 'media_player.join' and 'media_player.unjoin' actions instead." + }, + "deprecated_group_entities": { + "title": "Snapcast Groups Entities Deprecated", + "description": "Snapcast group entities are deprecated and will be removed in 2026.2. Please use the 'media_player.join' and 'media_player.unjoin' actions instead." + } } } diff --git a/homeassistant/components/snoo/coordinator.py b/homeassistant/components/snoo/coordinator.py index 8ce0db34621..43e717c2bc7 100644 --- a/homeassistant/components/snoo/coordinator.py +++ b/homeassistant/components/snoo/coordinator.py @@ -40,7 +40,7 @@ class SnooCoordinator(DataUpdateCoordinator[SnooData]): async def setup(self) -> None: """Perform setup needed on every coordintaor creation.""" - await self.snoo.subscribe(self.device, self.async_set_updated_data) + self.snoo.start_subscribe(self.device, self.async_set_updated_data) # After we subscribe - get the status so that we have something to start with. # We only need to do this once. The device will auto update otherwise. await self.snoo.get_status(self.device) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index 2afec990e4b..5a162a9e9d3 100644 --- a/homeassistant/components/snoo/manifest.json +++ b/homeassistant/components/snoo/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["snoo"], "quality_scale": "bronze", - "requirements": ["python-snoo==0.6.6"] + "requirements": ["python-snoo==0.8.3"] } diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 486b30edfd3..4a4101a2dd3 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["solarlog_cli"], "quality_scale": "platinum", - "requirements": ["solarlog_cli==0.4.0"] + "requirements": ["solarlog_cli==0.5.0"] } diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index c4bb119c006..a3a450fe49e 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from solarlog_cli.solarlog_models import InverterData, SolarlogData +from solarlog_cli.solarlog_models import BatteryData, InverterData, SolarlogData from homeassistant.components.sensor import ( SensorDeviceClass, @@ -35,6 +35,13 @@ class SolarLogCoordinatorSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[SolarlogData], StateType | datetime | None] +@dataclass(frozen=True, kw_only=True) +class SolarLogBatterySensorEntityDescription(SensorEntityDescription): + """Describes Solarlog battery sensor entity.""" + + value_fn: Callable[[BatteryData], float | int | None] + + @dataclass(frozen=True, kw_only=True) class SolarLogInverterSensorEntityDescription(SensorEntityDescription): """Describes Solarlog inverter sensor entity.""" @@ -247,6 +254,33 @@ SOLARLOG_SENSOR_TYPES: tuple[SolarLogCoordinatorSensorEntityDescription, ...] = ), ) +BATTERY_SENSOR_TYPES: tuple[SolarLogBatterySensorEntityDescription, ...] = ( + SolarLogBatterySensorEntityDescription( + key="charging_power", + translation_key="charging_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda battery_data: battery_data.charge_power, + ), + SolarLogBatterySensorEntityDescription( + key="discharging_power", + translation_key="discharging_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda battery_data: battery_data.discharge_power, + ), + SolarLogBatterySensorEntityDescription( + key="charge_level", + translation_key="charge_level", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda battery_data: battery_data.level, + ), +) + INVERTER_SENSOR_TYPES: tuple[SolarLogInverterSensorEntityDescription, ...] = ( SolarLogInverterSensorEntityDescription( key="current_power", @@ -286,6 +320,13 @@ async def async_setup_entry( for sensor in SOLARLOG_SENSOR_TYPES ] + # add battery sensors only if respective data is available (otherwise no battery attached to solarlog) + if coordinator.data.battery_data is not None: + entities.extend( + SolarLogBatterySensor(coordinator, sensor) + for sensor in BATTERY_SENSOR_TYPES + ) + device_data = coordinator.data.inverter_data if device_data: @@ -318,6 +359,19 @@ class SolarLogCoordinatorSensor(SolarLogCoordinatorEntity, SensorEntity): return self.entity_description.value_fn(self.coordinator.data) +class SolarLogBatterySensor(SolarLogCoordinatorEntity, SensorEntity): + """Represents a SolarLog battery sensor.""" + + entity_description: SolarLogBatterySensorEntityDescription + + @property + def native_value(self) -> StateType: + """Return the state for this sensor.""" + if (battery_data := self.coordinator.data.battery_data) is None: + return None + return self.entity_description.value_fn(battery_data) + + class SolarLogInverterSensor(SolarLogInverterEntity, SensorEntity): """Represents a SolarLog inverter sensor.""" diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json index bf87b0b0938..bba1380fb9f 100644 --- a/homeassistant/components/solarlog/strings.json +++ b/homeassistant/components/solarlog/strings.json @@ -58,6 +58,15 @@ }, "entity": { "sensor": { + "charge_level": { + "name": "Charge level" + }, + "charging_power": { + "name": "Charging power" + }, + "discharging_power": { + "name": "Discharging power" + }, "last_update": { "name": "Last update" }, diff --git a/homeassistant/components/somfy_mylink/strings.json b/homeassistant/components/somfy_mylink/strings.json index 90489c0ba34..ec501fac302 100644 --- a/homeassistant/components/somfy_mylink/strings.json +++ b/homeassistant/components/somfy_mylink/strings.json @@ -29,13 +29,13 @@ }, "step": { "init": { - "title": "Configure MyLink Options", + "title": "Configure MyLink options", "data": { "target_id": "Configure options for a cover." } }, "target_config": { - "title": "Configure MyLink Cover", + "title": "Configure MyLink cover", "description": "Configure options for `{target_name}`", "data": { "reverse": "Cover is reversed" diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 76e0a915060..ac2e3f50f13 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -15,6 +15,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ] @@ -154,6 +155,7 @@ SONOS_CREATE_AUDIO_FORMAT_SENSOR = "sonos_create_audio_format_sensor" SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_FAVORITES_SENSOR = "sonos_create_favorites_sensor" SONOS_CREATE_MIC_SENSOR = "sonos_create_mic_sensor" +SONOS_CREATE_SELECTS = "sonos_create_selects" SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" @@ -186,6 +188,12 @@ MODELS_TV_ONLY = ( "ULTRA", ) MODELS_LINEIN_AND_TV = ("AMP",) +MODEL_SONOS_ARC_ULTRA = "SONOS ARC ULTRA" + +ATTR_SPEECH_ENHANCEMENT_ENABLED = "speech_enhance_enabled" +SPEECH_DIALOG_LEVEL = "speech_dialog_level" +ATTR_DIALOG_LEVEL = "dialog_level" +ATTR_DIALOG_LEVEL_ENUM = "dialog_level_enum" AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 6fb7bf00589..0b30c820da3 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -793,8 +793,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if one_alarm.alarm_id == str(alarm_id): alarm = one_alarm if alarm is None: - _LOGGER.warning("Did not find alarm with id %s", alarm_id) - return + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_alarm_id", + translation_placeholders={ + "alarm_id": str(alarm_id), + }, + ) if time is not None: alarm.start_time = time if volume is not None: diff --git a/homeassistant/components/sonos/quality_scale.yaml b/homeassistant/components/sonos/quality_scale.yaml new file mode 100644 index 00000000000..5899503ae8d --- /dev/null +++ b/homeassistant/components/sonos/quality_scale.yaml @@ -0,0 +1,76 @@ +rules: + # Bronze + action-setup: done + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: done + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: + status: exempt + comment: | + Setup is done through discovery and does not require a test before setup. + unique-config-entry: + status: exempt + comment: | + Integration only supports and uses a single config entry. Exempting because hassfest check is incomplete. + # Silver + action-exceptions: todo + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: todo + reauthentication-flow: + status: exempt + comment: | + No authentication + test-coverage: + status: todo + comment: | + test_play_media_library if statements in the tests + PR #147064 + test_sensor is testing both binary sensor and sensor + tests using internals + # Gold + devices: done + diagnostics: done + discovery-update-info: todo + discovery: done + docs-data-update: todo + docs-examples: done + docs-known-limitations: done + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: done + dynamic-devices: done + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: todo + exception-translations: todo + icon-translations: todo + reconfiguration-flow: + status: exempt + comment: | + No configurable options + repair-issues: todo + stale-devices: todo + + # Platinum + async-dependency: todo + inject-websession: todo + strict-typing: todo diff --git a/homeassistant/components/sonos/select.py b/homeassistant/components/sonos/select.py new file mode 100644 index 00000000000..052a1d87967 --- /dev/null +++ b/homeassistant/components/sonos/select.py @@ -0,0 +1,129 @@ +"""Select entities for Sonos.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import ( + ATTR_DIALOG_LEVEL, + ATTR_DIALOG_LEVEL_ENUM, + MODEL_SONOS_ARC_ULTRA, + SONOS_CREATE_SELECTS, + SPEECH_DIALOG_LEVEL, +) +from .entity import SonosEntity +from .helpers import SonosConfigEntry, soco_error +from .speaker import SonosSpeaker + + +@dataclass(frozen=True, kw_only=True) +class SonosSelectEntityDescription(SelectEntityDescription): + """Describes AirGradient select entity.""" + + soco_attribute: str + speaker_attribute: str + speaker_model: str + + +SELECT_TYPES: list[SonosSelectEntityDescription] = [ + SonosSelectEntityDescription( + key=SPEECH_DIALOG_LEVEL, + translation_key=SPEECH_DIALOG_LEVEL, + soco_attribute=ATTR_DIALOG_LEVEL, + speaker_attribute=ATTR_DIALOG_LEVEL_ENUM, + speaker_model=MODEL_SONOS_ARC_ULTRA, + options=["off", "low", "medium", "high", "max"], + ), +] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: SonosConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Sonos select platform from a config entry.""" + + def available_soco_attributes( + speaker: SonosSpeaker, + ) -> list[SonosSelectEntityDescription]: + features: list[SonosSelectEntityDescription] = [] + for select_data in SELECT_TYPES: + if select_data.speaker_model == speaker.model_name.upper(): + if ( + state := getattr(speaker.soco, select_data.soco_attribute, None) + ) is not None: + setattr(speaker, select_data.speaker_attribute, state) + features.append(select_data) + return features + + async def _async_create_entities(speaker: SonosSpeaker) -> None: + available_features = await hass.async_add_executor_job( + available_soco_attributes, speaker + ) + async_add_entities( + SonosSelectEntity(speaker, config_entry, select_data) + for select_data in available_features + ) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_SELECTS, _async_create_entities) + ) + + +class SonosSelectEntity(SonosEntity, SelectEntity): + """Representation of a Sonos select entity.""" + + def __init__( + self, + speaker: SonosSpeaker, + config_entry: SonosConfigEntry, + select_data: SonosSelectEntityDescription, + ) -> None: + """Initialize the select entity.""" + super().__init__(speaker, config_entry) + self._attr_unique_id = f"{self.soco.uid}-{select_data.key}" + self._attr_translation_key = select_data.translation_key + assert select_data.options is not None + self._attr_options = select_data.options + self.speaker_attribute = select_data.speaker_attribute + self.soco_attribute = select_data.soco_attribute + + async def _async_fallback_poll(self) -> None: + """Poll the value if subscriptions are not working.""" + await self.hass.async_add_executor_job(self.poll_state) + self.async_write_ha_state() + + @soco_error() + def poll_state(self) -> None: + """Poll the device for the current state.""" + state = getattr(self.soco, self.soco_attribute) + setattr(self.speaker, self.speaker_attribute, state) + + @property + def current_option(self) -> str | None: + """Return the current option for the entity.""" + option = getattr(self.speaker, self.speaker_attribute, None) + if not isinstance(option, int) or not (0 <= option < len(self._attr_options)): + _LOGGER.error( + "Invalid option %s for %s on %s", + option, + self.soco_attribute, + self.speaker.zone_name, + ) + return None + return self._attr_options[option] + + @soco_error() + def select_option(self, option: str) -> None: + """Set a new value.""" + dialog_level = self._attr_options.index(option) + setattr(self.soco, self.soco_attribute, dialog_level) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index f5cfb84ec36..427f02f0479 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -35,6 +35,8 @@ from homeassistant.util import dt as dt_util from .alarms import SonosAlarms from .const import ( + ATTR_DIALOG_LEVEL, + ATTR_SPEECH_ENHANCEMENT_ENABLED, AVAILABILITY_TIMEOUT, BATTERY_SCAN_INTERVAL, DOMAIN, @@ -46,6 +48,7 @@ from .const import ( SONOS_CREATE_LEVELS, SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_MIC_SENSOR, + SONOS_CREATE_SELECTS, SONOS_CREATE_SWITCHES, SONOS_FALLBACK_POLL, SONOS_REBOOTED, @@ -157,6 +160,8 @@ class SonosSpeaker: # Home theater self.audio_delay: int | None = None self.dialog_level: bool | None = None + self.dialog_level_enum: int | None = None + self.speech_enhance_enabled: bool | None = None self.night_mode: bool | None = None self.sub_enabled: bool | None = None self.sub_crossover: int | None = None @@ -251,6 +256,7 @@ class SonosSpeaker: ]: dispatches.append((SONOS_CREATE_ALARM, self, new_alarms)) + dispatches.append((SONOS_CREATE_SELECTS, self)) dispatches.append((SONOS_CREATE_SWITCHES, self)) dispatches.append((SONOS_CREATE_MEDIA_PLAYER, self)) dispatches.append((SONOS_SPEAKER_ADDED, self.soco.uid)) @@ -548,6 +554,11 @@ class SonosSpeaker: @callback def async_update_volume(self, event: SonosEvent) -> None: """Update information about currently volume settings.""" + _LOGGER.debug( + "Updating volume for %s with event variables: %s", + self.zone_name, + event.variables, + ) self.event_stats.process(event) variables = event.variables @@ -565,6 +576,7 @@ class SonosSpeaker: for bool_var in ( "dialog_level", + ATTR_SPEECH_ENHANCEMENT_ENABLED, "night_mode", "sub_enabled", "surround_enabled", @@ -585,6 +597,10 @@ class SonosSpeaker: if int_var in variables: setattr(self, int_var, variables[int_var]) + for enum_var in (ATTR_DIALOG_LEVEL,): + if enum_var in variables: + setattr(self, f"{enum_var}_enum", variables[enum_var]) + self.async_write_entity_states() # diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index b2f20449beb..adb233519b2 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -50,6 +50,18 @@ "name": "Music surround level" } }, + "select": { + "speech_dialog_level": { + "name": "Speech enhancement", + "state": { + "off": "[%key:common::state::off%]", + "low": "[%key:common::state::low%]", + "medium": "[%key:common::state::medium%]", + "high": "[%key:common::state::high%]", + "max": "Max" + } + } + }, "sensor": { "audio_input_format": { "name": "Audio input format" @@ -211,6 +223,9 @@ }, "timeout_join": { "message": "Timeout while waiting for Sonos player to join the group {group_description}" + }, + "invalid_alarm_id": { + "message": "Alarm {alarm_id} does not exist and cannot be updated." } } } diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 582845d10a2..653be229b22 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -19,7 +19,9 @@ from homeassistant.helpers.event import async_track_time_change from .alarms import SonosAlarms from .const import ( + ATTR_SPEECH_ENHANCEMENT_ENABLED, DOMAIN, + MODEL_SONOS_ARC_ULTRA, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM, SONOS_CREATE_SWITCHES, @@ -59,6 +61,7 @@ ALL_FEATURES = ( ATTR_SURROUND_ENABLED, ATTR_STATUS_LIGHT, ) +ALL_SUBST_FEATURES = (ATTR_SPEECH_ENHANCEMENT_ENABLED,) COORDINATOR_FEATURES = ATTR_CROSSFADE @@ -69,6 +72,14 @@ POLL_REQUIRED = ( WEEKEND_DAYS = (0, 6) +# Mapping of model names to feature attributes that need to be substituted. +# This is used to handle differences in attributes across Sonos models. +MODEL_FEATURE_SUBSTITUTIONS: dict[str, dict[str, str]] = { + MODEL_SONOS_ARC_ULTRA: { + ATTR_SPEECH_ENHANCEMENT: ATTR_SPEECH_ENHANCEMENT_ENABLED, + }, +} + async def async_setup_entry( hass: HomeAssistant, @@ -92,6 +103,13 @@ async def async_setup_entry( def available_soco_attributes(speaker: SonosSpeaker) -> list[str]: features = [] + for feature_type in ALL_SUBST_FEATURES: + try: + if (state := getattr(speaker.soco, feature_type, None)) is not None: + setattr(speaker, feature_type, state) + except SoCoSlaveException: + pass + for feature_type in ALL_FEATURES: try: if (state := getattr(speaker.soco, feature_type, None)) is not None: @@ -107,12 +125,23 @@ async def async_setup_entry( available_soco_attributes, speaker ) for feature_type in available_features: + attribute_key = MODEL_FEATURE_SUBSTITUTIONS.get( + speaker.model_name.upper(), {} + ).get(feature_type, feature_type) _LOGGER.debug( - "Creating %s switch on %s", + "Creating %s switch on %s attribute %s", feature_type, speaker.zone_name, + attribute_key, + ) + entities.append( + SonosSwitchEntity( + feature_type=feature_type, + attribute_key=attribute_key, + speaker=speaker, + config_entry=config_entry, + ) ) - entities.append(SonosSwitchEntity(feature_type, speaker, config_entry)) async_add_entities(entities) config_entry.async_on_unload( @@ -127,11 +156,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): """Representation of a Sonos feature switch.""" def __init__( - self, feature_type: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry + self, + feature_type: str, + attribute_key: str, + speaker: SonosSpeaker, + config_entry: SonosConfigEntry, ) -> None: """Initialize the switch.""" super().__init__(speaker, config_entry) - self.feature_type = feature_type + self.attribute_key = attribute_key self.needs_coordinator = feature_type in COORDINATOR_FEATURES self._attr_entity_category = EntityCategory.CONFIG self._attr_translation_key = feature_type @@ -149,15 +182,15 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): @soco_error() def poll_state(self) -> None: """Poll the current state of the switch.""" - state = getattr(self.soco, self.feature_type) - setattr(self.speaker, self.feature_type, state) + state = getattr(self.soco, self.attribute_key) + setattr(self.speaker, self.attribute_key, state) @property def is_on(self) -> bool: """Return True if entity is on.""" if self.needs_coordinator and not self.speaker.is_coordinator: - return cast(bool, getattr(self.speaker.coordinator, self.feature_type)) - return cast(bool, getattr(self.speaker, self.feature_type)) + return cast(bool, getattr(self.speaker.coordinator, self.attribute_key)) + return cast(bool, getattr(self.speaker, self.attribute_key)) def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" @@ -175,7 +208,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): else: soco = self.soco try: - setattr(soco, self.feature_type, enable) + setattr(soco, self.attribute_key, enable) except SoCoUPnPException as exc: _LOGGER.warning("Could not toggle %s: %s", self.entity_id, exc) diff --git a/homeassistant/components/speedtestdotnet/coordinator.py b/homeassistant/components/speedtestdotnet/coordinator.py index 1308cb1d825..fac78a113f2 100644 --- a/homeassistant/components/speedtestdotnet/coordinator.py +++ b/homeassistant/components/speedtestdotnet/coordinator.py @@ -29,11 +29,10 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): api: speedtest.Speedtest, ) -> None: """Initialize the data object.""" - self.hass = hass self.api = api self.servers: dict[str, dict] = {DEFAULT_SERVER: {}} super().__init__( - self.hass, + hass, _LOGGER, config_entry=config_entry, name=DOMAIN, diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 80fcc777e73..ac7f575bcc5 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -8,5 +8,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["spotifyaio"], - "requirements": ["spotifyaio==0.8.11"] + "requirements": ["spotifyaio==1.0.0"] } diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 4f2a1fa7aa5..cebd4fcb04f 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -157,26 +157,28 @@ class BrowseData: cmd = ["apps", 0, browse_limit] result = await player.async_query(*cmd) - for app in result["appss_loop"]: - app_cmd = "app-" + app["cmd"] - if app_cmd not in self.known_apps_radios: - self.add_new_command(app_cmd, "item_id") - _LOGGER.debug( - "Adding new command %s to browse data for player %s", - app_cmd, - player.player_id, - ) + if result and result.get("appss_loop"): + for app in result["appss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) cmd = ["radios", 0, browse_limit] result = await player.async_query(*cmd) - for app in result["radioss_loop"]: - app_cmd = "app-" + app["cmd"] - if app_cmd not in self.known_apps_radios: - self.add_new_command(app_cmd, "item_id") - _LOGGER.debug( - "Adding new command %s to browse data for player %s", - app_cmd, - player.player_id, - ) + if result and result.get("radioss_loop"): + for app in result["radioss_loop"]: + app_cmd = "app-" + app["cmd"] + if app_cmd not in self.known_apps_radios: + self.add_new_command(app_cmd, "item_id") + _LOGGER.debug( + "Adding new command %s to browse data for player %s", + app_cmd, + player.player_id, + ) def _build_response_apps_radios_category( diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 49aad4fd698..a857602a584 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -325,8 +325,8 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): @property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" - if self._player.volume: - return int(float(self._player.volume)) / 100.0 + if self._player.volume is not None: + return float(self._player.volume) / 100.0 return None @@ -435,7 +435,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - volume_percent = str(int(volume * 100)) + volume_percent = str(round(volume * 100)) await self._player.async_set_volume(volume_percent) await self.coordinator.async_refresh() diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index f8846c2a97f..f940971c15c 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -2,8 +2,6 @@ from __future__ import annotations -from collections.abc import Callable - from homeassistant.helpers.entity import Entity from .account import StarlineAccount, StarlineDevice @@ -24,7 +22,6 @@ class StarlineEntity(Entity): self._key = key self._attr_unique_id = f"starline-{key}-{device.device_id}" self._attr_device_info = account.device_info(device) - self._unsubscribe_api: Callable | None = None @property def available(self) -> bool: @@ -38,11 +35,4 @@ class StarlineEntity(Entity): async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() - self._unsubscribe_api = self._account.api.add_update_listener(self.update) - - async def async_will_remove_from_hass(self) -> None: - """Call when entity is being removed from Home Assistant.""" - await super().async_will_remove_from_hass() - if self._unsubscribe_api is not None: - self._unsubscribe_api() - self._unsubscribe_api = None + self.async_on_remove(self._account.api.add_update_listener(self.update)) diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index d2824ab10e5..a196364313a 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -98,7 +98,7 @@ async def _async_import(hass: HomeAssistant, config: ConfigType) -> None: hass, DOMAIN, "deprecated_yaml", - breaks_in_ha_version="2025.9.0", + breaks_in_ha_version="2025.11.0", is_fixable=False, issue_domain=DOMAIN, severity=ir.IssueSeverity.WARNING, diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index f8140ed36d7..7418c5b7b32 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", "iot_class": "local_polling", "loggers": ["pymodbus", "pystiebeleltron"], - "requirements": ["pystiebeleltron==0.1.0"] + "requirements": ["pystiebeleltron==0.2.3"] } diff --git a/homeassistant/components/switchbot/icons.json b/homeassistant/components/switchbot/icons.json index 2aef019aab4..cf9217bf70b 100644 --- a/homeassistant/components/switchbot/icons.json +++ b/homeassistant/components/switchbot/icons.json @@ -1,6 +1,21 @@ { "entity": { "sensor": { + "light_level": { + "default": "mdi:brightness-7", + "state": { + "1": "mdi:brightness-1", + "2": "mdi:brightness-1", + "3": "mdi:brightness-2", + "4": "mdi:brightness-3", + "5": "mdi:brightness-4", + "6": "mdi:brightness-5", + "7": "mdi:brightness-5", + "8": "mdi:brightness-6", + "9": "mdi:brightness-6", + "10": "mdi:brightness-7" + } + }, "water_level": { "default": "mdi:water-percent", "state": { diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 6ed11acda08..175aacf5d4c 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -41,5 +41,5 @@ "iot_class": "local_push", "loggers": ["switchbot"], "quality_scale": "gold", - "requirements": ["PySwitchbot==0.68.3"] + "requirements": ["PySwitchbot==0.69.0"] } diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index f6c5d526ab7..9196453e98c 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -67,7 +67,6 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { "lightLevel": SensorEntityDescription( key="lightLevel", translation_key="light_level", - native_unit_of_measurement="Level", state_class=SensorStateClass.MEASUREMENT, ), "humidity": SensorEntityDescription( diff --git a/homeassistant/components/switchbot/vacuum.py b/homeassistant/components/switchbot/vacuum.py index 9dade6b7f46..8535fdc7843 100644 --- a/homeassistant/components/switchbot/vacuum.py +++ b/homeassistant/components/switchbot/vacuum.py @@ -87,8 +87,7 @@ class SwitchbotVacuumEntity(SwitchbotEntity, StateVacuumEntity): _device: switchbot.SwitchbotVacuum _attr_supported_features = ( - VacuumEntityFeature.BATTERY - | VacuumEntityFeature.RETURN_HOME + VacuumEntityFeature.RETURN_HOME | VacuumEntityFeature.START | VacuumEntityFeature.STATE ) @@ -108,11 +107,6 @@ class SwitchbotVacuumEntity(SwitchbotEntity, StateVacuumEntity): status_code = self._device.get_work_status() return SWITCHBOT_VACUUM_STATE_MAP[self.protocol_version].get(status_code) - @property - def battery_level(self) -> int: - """Return the vacuum battery.""" - return self._device.get_battery() - async def async_start(self) -> None: """Start or resume the cleaning task.""" self._last_run_success = bool( diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index ae3a32997ae..edf30984fe6 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -29,6 +29,7 @@ PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, + Platform.COVER, Platform.FAN, Platform.LIGHT, Platform.LOCK, @@ -47,6 +48,7 @@ class SwitchbotDevices: ) buttons: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) climates: list[tuple[Remote, SwitchBotCoordinator]] = field(default_factory=list) + covers: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) switches: list[tuple[Device | Remote, SwitchBotCoordinator]] = field( default_factory=list ) @@ -182,6 +184,11 @@ async def make_device_data( devices_data.buttons.append((device, coordinator)) else: devices_data.switches.append((device, coordinator)) + if isinstance(device, Device) and device.device_type.startswith("Air Purifier"): + coordinator = await coordinator_for_device( + hass, entry, api, device, coordinators_by_id + ) + devices_data.fans.append((device, coordinator)) if isinstance(device, Device) and device.device_type in [ "Battery Circulator Fan", @@ -192,6 +199,27 @@ async def make_device_data( ) devices_data.fans.append((device, coordinator)) devices_data.sensors.append((device, coordinator)) + if isinstance(device, Device) and device.device_type in [ + "Curtain", + "Curtain3", + "Roller Shade", + "Blind Tilt", + ]: + coordinator = await coordinator_for_device( + hass, entry, api, device, coordinators_by_id + ) + devices_data.covers.append((device, coordinator)) + devices_data.binary_sensors.append((device, coordinator)) + devices_data.sensors.append((device, coordinator)) + + if isinstance(device, Device) and device.device_type in [ + "Garage Door Opener", + ]: + coordinator = await coordinator_for_device( + hass, entry, api, device, coordinators_by_id + ) + devices_data.covers.append((device, coordinator)) + devices_data.binary_sensors.append((device, coordinator)) if isinstance(device, Device) and device.device_type in [ "Strip Light", diff --git a/homeassistant/components/switchbot_cloud/binary_sensor.py b/homeassistant/components/switchbot_cloud/binary_sensor.py index cd0e6e8968c..a1ad6d6887d 100644 --- a/homeassistant/components/switchbot_cloud/binary_sensor.py +++ b/homeassistant/components/switchbot_cloud/binary_sensor.py @@ -60,6 +60,11 @@ BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = { CALIBRATION_DESCRIPTION, DOOR_OPEN_DESCRIPTION, ), + "Curtain": (CALIBRATION_DESCRIPTION,), + "Curtain3": (CALIBRATION_DESCRIPTION,), + "Roller Shade": (CALIBRATION_DESCRIPTION,), + "Blind Tilt": (CALIBRATION_DESCRIPTION,), + "Garage Door Opener": (DOOR_OPEN_DESCRIPTION,), } diff --git a/homeassistant/components/switchbot_cloud/const.py b/homeassistant/components/switchbot_cloud/const.py index dcca5119a74..23a212075c4 100644 --- a/homeassistant/components/switchbot_cloud/const.py +++ b/homeassistant/components/switchbot_cloud/const.py @@ -1,6 +1,7 @@ """Constants for the SwitchBot Cloud integration.""" from datetime import timedelta +from enum import Enum from typing import Final DOMAIN: Final = "switchbot_cloud" @@ -17,3 +18,18 @@ VACUUM_FAN_SPEED_STRONG = "strong" VACUUM_FAN_SPEED_MAX = "max" AFTER_COMMAND_REFRESH = 5 +COVER_ENTITY_AFTER_COMMAND_REFRESH = 10 + + +class AirPurifierMode(Enum): + """Air Purifier Modes.""" + + NORMAL = 1 + AUTO = 2 + SLEEP = 3 + PET = 4 + + @classmethod + def get_modes(cls) -> list[str]: + """Return a list of available air purifier modes as lowercase strings.""" + return [mode.name.lower() for mode in cls] diff --git a/homeassistant/components/switchbot_cloud/cover.py b/homeassistant/components/switchbot_cloud/cover.py new file mode 100644 index 00000000000..77f0b960d25 --- /dev/null +++ b/homeassistant/components/switchbot_cloud/cover.py @@ -0,0 +1,233 @@ +"""Support for the Switchbot BlindTilt, Curtain, Curtain3, RollerShade as Cover.""" + +import asyncio +from typing import Any + +from switchbot_api import ( + BlindTiltCommands, + CommonCommands, + CurtainCommands, + Device, + Remote, + RollerShadeCommands, + SwitchBotAPI, +) + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import SwitchbotCloudData, SwitchBotCoordinator +from .const import COVER_ENTITY_AFTER_COMMAND_REFRESH, DOMAIN +from .entity import SwitchBotCloudEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up SwitchBot Cloud entry.""" + data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] + async_add_entities( + _async_make_entity(data.api, device, coordinator) + for device, coordinator in data.devices.covers + ) + + +class SwitchBotCloudCover(SwitchBotCloudEntity, CoverEntity): + """Representation of a SwitchBot Cover.""" + + _attr_name = None + _attr_is_closed: bool | None = None + + def _set_attributes(self) -> None: + if self.coordinator.data is None: + return + position: int | None = self.coordinator.data.get("slidePosition") + if position is None: + return + self._attr_current_cover_position = 100 - position + self._attr_current_cover_tilt_position = 100 - position + self._attr_is_closed = position == 100 + + +class SwitchBotCloudCoverCurtain(SwitchBotCloudCover): + """Representation of a SwitchBot Curtain & Curtain3.""" + + _attr_device_class = CoverDeviceClass.CURTAIN + _attr_supported_features: CoverEntityFeature = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + ) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.send_api_command(CommonCommands.ON) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self.send_api_command(CommonCommands.OFF) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + position: int | None = kwargs.get("position") + if position is not None: + await self.send_api_command( + CurtainCommands.SET_POSITION, + parameters=f"{0},ff,{100 - position}", + ) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self.send_api_command(CurtainCommands.PAUSE) + await self.coordinator.async_request_refresh() + + +class SwitchBotCloudCoverRollerShade(SwitchBotCloudCover): + """Representation of a SwitchBot RollerShade.""" + + _attr_device_class = CoverDeviceClass.SHADE + _attr_supported_features: CoverEntityFeature = ( + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + ) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.send_api_command(RollerShadeCommands.SET_POSITION, parameters=str(0)) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self.send_api_command( + RollerShadeCommands.SET_POSITION, parameters=str(100) + ) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + position: int | None = kwargs.get("position") + if position is not None: + await self.send_api_command( + RollerShadeCommands.SET_POSITION, parameters=str(100 - position) + ) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + +class SwitchBotCloudCoverBlindTilt(SwitchBotCloudCover): + """Representation of a SwitchBot Blind Tilt.""" + + _attr_direction: str | None = None + _attr_device_class = CoverDeviceClass.BLIND + _attr_supported_features: CoverEntityFeature = ( + CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + ) + + def _set_attributes(self) -> None: + if self.coordinator.data is None: + return + position: int | None = self.coordinator.data.get("slidePosition") + if position is None: + return + self._attr_is_closed = position in [0, 100] + if position > 50: + percent = 100 - ((position - 50) * 2) + else: + percent = 100 - (50 - position) * 2 + self._attr_current_cover_position = percent + self._attr_current_cover_tilt_position = percent + direction = self.coordinator.data.get("direction") + self._attr_direction = direction.lower() if direction else None + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + percent: int | None = kwargs.get("tilt_position") + if percent is not None: + await self.send_api_command( + BlindTiltCommands.SET_POSITION, + parameters=f"{self._attr_direction};{percent}", + ) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.send_api_command(BlindTiltCommands.FULLY_OPEN) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the cover.""" + if self._attr_direction is not None: + if "up" in self._attr_direction: + await self.send_api_command(BlindTiltCommands.CLOSE_UP) + else: + await self.send_api_command(BlindTiltCommands.CLOSE_DOWN) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + +class SwitchBotCloudCoverGarageDoorOpener(SwitchBotCloudCover): + """Representation of a SwitchBot Garage Door Opener.""" + + _attr_device_class = CoverDeviceClass.GARAGE + _attr_supported_features: CoverEntityFeature = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + + def _set_attributes(self) -> None: + if self.coordinator.data is None: + return + door_status: int | None = self.coordinator.data.get("doorStatus") + self._attr_is_closed = None if door_status is None else door_status == 1 + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.send_api_command(CommonCommands.ON) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self.send_api_command(CommonCommands.OFF) + await asyncio.sleep(COVER_ENTITY_AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + +@callback +def _async_make_entity( + api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator +) -> ( + SwitchBotCloudCoverBlindTilt + | SwitchBotCloudCoverRollerShade + | SwitchBotCloudCoverCurtain + | SwitchBotCloudCoverGarageDoorOpener +): + """Make a SwitchBotCloudCover device.""" + if device.device_type == "Blind Tilt": + return SwitchBotCloudCoverBlindTilt(api, device, coordinator) + if device.device_type == "Roller Shade": + return SwitchBotCloudCoverRollerShade(api, device, coordinator) + if device.device_type == "Garage Door Opener": + return SwitchBotCloudCoverGarageDoorOpener(api, device, coordinator) + return SwitchBotCloudCoverCurtain(api, device, coordinator) diff --git a/homeassistant/components/switchbot_cloud/fan.py b/homeassistant/components/switchbot_cloud/fan.py index d7cf82520ec..9424b5478ac 100644 --- a/homeassistant/components/switchbot_cloud/fan.py +++ b/homeassistant/components/switchbot_cloud/fan.py @@ -1,23 +1,30 @@ """Support for the Switchbot Battery Circulator fan.""" import asyncio +import logging from typing import Any from switchbot_api import ( + AirPurifierCommands, BatteryCirculatorFanCommands, BatteryCirculatorFanMode, CommonCommands, + SwitchBotAPI, ) from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import SwitchbotCloudData -from .const import DOMAIN +from .const import AFTER_COMMAND_REFRESH, DOMAIN, AirPurifierMode from .entity import SwitchBotCloudEntity +_LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, @@ -26,10 +33,13 @@ async def async_setup_entry( ) -> None: """Set up SwitchBot Cloud entry.""" data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] - async_add_entities( - SwitchBotCloudFan(data.api, device, coordinator) - for device, coordinator in data.devices.fans - ) + for device, coordinator in data.devices.fans: + if device.device_type.startswith("Air Purifier"): + async_add_entities( + [SwitchBotAirPurifierEntity(data.api, device, coordinator)] + ) + else: + async_add_entities([SwitchBotCloudFan(data.api, device, coordinator)]) class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity): @@ -37,6 +47,7 @@ class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity): _attr_name = None + _api: SwitchBotAPI _attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE @@ -88,13 +99,13 @@ class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity): command=BatteryCirculatorFanCommands.SET_WIND_SPEED, parameters=str(self.percentage), ) - await asyncio.sleep(5) + await asyncio.sleep(AFTER_COMMAND_REFRESH) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" await self.send_api_command(CommonCommands.OFF) - await asyncio.sleep(5) + await asyncio.sleep(AFTER_COMMAND_REFRESH) await self.coordinator.async_request_refresh() async def async_set_percentage(self, percentage: int) -> None: @@ -107,7 +118,7 @@ class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity): command=BatteryCirculatorFanCommands.SET_WIND_SPEED, parameters=str(percentage), ) - await asyncio.sleep(5) + await asyncio.sleep(AFTER_COMMAND_REFRESH) await self.coordinator.async_request_refresh() async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -116,5 +127,77 @@ class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity): command=BatteryCirculatorFanCommands.SET_WIND_MODE, parameters=preset_mode, ) - await asyncio.sleep(5) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + +class SwitchBotAirPurifierEntity(SwitchBotCloudEntity, FanEntity): + """Representation of a Switchbot air purifier.""" + + _api: SwitchBotAPI + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) + _attr_preset_modes = AirPurifierMode.get_modes() + _attr_translation_key = "air_purifier" + _attr_name = None + _attr_is_on: bool | None = None + + @property + def is_on(self) -> bool | None: + """Return true if device is on.""" + return self._attr_is_on + + def _set_attributes(self) -> None: + """Set attributes from coordinator data.""" + if self.coordinator.data is None: + return + + self._attr_is_on = self.coordinator.data.get("power") == STATE_ON.upper() + mode = self.coordinator.data.get("mode") + self._attr_preset_mode = ( + AirPurifierMode(mode).name.lower() if mode is not None else None + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the air purifier.""" + + _LOGGER.debug( + "Switchbot air purifier to set preset mode %s %s", + preset_mode, + self._attr_unique_id, + ) + await self.send_api_command( + AirPurifierCommands.SET_MODE, + parameters={"mode": AirPurifierMode[preset_mode.upper()].value}, + ) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the air purifier.""" + + _LOGGER.debug( + "Switchbot air purifier to set turn on %s %s %s", + percentage, + preset_mode, + self._attr_unique_id, + ) + await self.send_api_command(CommonCommands.ON) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the air purifier.""" + + _LOGGER.debug("Switchbot air purifier to set turn off %s", self._attr_unique_id) + await self.send_api_command(CommonCommands.OFF) + await asyncio.sleep(AFTER_COMMAND_REFRESH) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/switchbot_cloud/icons.json b/homeassistant/components/switchbot_cloud/icons.json new file mode 100644 index 00000000000..2a13cbe7579 --- /dev/null +++ b/homeassistant/components/switchbot_cloud/icons.json @@ -0,0 +1,22 @@ +{ + "entity": { + "fan": { + "air_purifier": { + "default": "mdi:air-purifier", + "state": { + "off": "mdi:air-purifier-off" + }, + "state_attributes": { + "preset_mode": { + "state": { + "normal": "mdi:fan", + "auto": "mdi:auto-mode", + "pet": "mdi:paw", + "sleep": "mdi:power-sleep" + } + } + } + } + } + } +} diff --git a/homeassistant/components/switchbot_cloud/sensor.py b/homeassistant/components/switchbot_cloud/sensor.py index 75e994b484e..163b1653686 100644 --- a/homeassistant/components/switchbot_cloud/sensor.py +++ b/homeassistant/components/switchbot_cloud/sensor.py @@ -139,6 +139,10 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = { "Smart Lock Lite": (BATTERY_DESCRIPTION,), "Smart Lock Pro": (BATTERY_DESCRIPTION,), "Smart Lock Ultra": (BATTERY_DESCRIPTION,), + "Curtain": (BATTERY_DESCRIPTION,), + "Curtain3": (BATTERY_DESCRIPTION,), + "Roller Shade": (BATTERY_DESCRIPTION,), + "Blind Tilt": (BATTERY_DESCRIPTION,), } diff --git a/homeassistant/components/switchbot_cloud/strings.json b/homeassistant/components/switchbot_cloud/strings.json index 11e92e6dfa3..adb7de00682 100644 --- a/homeassistant/components/switchbot_cloud/strings.json +++ b/homeassistant/components/switchbot_cloud/strings.json @@ -16,5 +16,21 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "fan": { + "air_purifier": { + "state_attributes": { + "preset_mode": { + "state": { + "normal": "[%key:common::state::normal%]", + "auto": "[%key:common::state::auto%]", + "pet": "Pet", + "sleep": "Sleep" + } + } + } + } + } } } diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 2799cf31fdd..c19f36f14dd 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -9,6 +9,6 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["systembridgeconnector"], - "requirements": ["systembridgeconnector==4.1.5", "systembridgemodels==4.2.4"], + "requirements": ["systembridgeconnector==4.1.10"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index cab147162aa..50c721e5f37 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_SOURCE, CONF_URL, + Platform, ) from homeassistant.core import ( HomeAssistant, @@ -291,6 +292,8 @@ MODULES: dict[str, ModuleType] = { PLATFORM_WEBHOOKS: webhooks, } +PLATFORMS: list[Platform] = [Platform.NOTIFY] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Telegram bot component.""" @@ -477,15 +480,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: TelegramBotConfigEntry) ) entry.runtime_data = notify_service + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) return True async def update_listener(hass: HomeAssistant, entry: TelegramBotConfigEntry) -> None: - """Handle options update.""" + """Handle config changes.""" entry.runtime_data.parse_mode = entry.options[ATTR_PARSER] + # reload entities + await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + async def async_unload_entry( hass: HomeAssistant, entry: TelegramBotConfigEntry @@ -494,4 +503,5 @@ async def async_unload_entry( # broadcast platform has no app if entry.runtime_data.app: await entry.runtime_data.app.shutdown() - return True + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/telegram_bot/notify.py b/homeassistant/components/telegram_bot/notify.py new file mode 100644 index 00000000000..822bd7b925d --- /dev/null +++ b/homeassistant/components/telegram_bot/notify.py @@ -0,0 +1,62 @@ +"""Telegram bot notification entity.""" + +from typing import Any + +import telegram + +from homeassistant.components.notify import NotifyEntity, NotifyEntityFeature +from homeassistant.config_entries import ConfigSubentry +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import TelegramBotConfigEntry +from .const import ATTR_TITLE, CONF_CHAT_ID, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: TelegramBotConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the telegram bot notification entity platform.""" + + for subentry_id, subentry in config_entry.subentries.items(): + async_add_entities( + [TelegramBotNotifyEntity(config_entry, subentry)], + config_subentry_id=subentry_id, + ) + + +class TelegramBotNotifyEntity(NotifyEntity): + """Representation of a telegram bot notification entity.""" + + _attr_supported_features = NotifyEntityFeature.TITLE + + def __init__( + self, + config_entry: TelegramBotConfigEntry, + subentry: ConfigSubentry, + ) -> None: + """Initialize a notification entity.""" + bot_id = config_entry.runtime_data.bot.id + chat_id = subentry.data[CONF_CHAT_ID] + + self._attr_unique_id = f"{bot_id}_{chat_id}" + self.name = subentry.title + + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer="Telegram", + model=config_entry.data[CONF_PLATFORM].capitalize(), + sw_version=telegram.__version__, + identifiers={(DOMAIN, f"{bot_id}")}, + ) + self._target = chat_id + self._service = config_entry.runtime_data + + async def async_send_message(self, message: str, title: str | None = None) -> None: + """Send a message.""" + kwargs: dict[str, Any] = {ATTR_TITLE: title} + await self._service.send_message(message, self._target, self._context, **kwargs) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index a3311c35563..ad2402bb980 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -17,6 +17,7 @@ from homeassistant.components.blueprint import ( ) from homeassistant.components.button import DOMAIN as DOMAIN_BUTTON from homeassistant.components.cover import DOMAIN as DOMAIN_COVER +from homeassistant.components.event import DOMAIN as DOMAIN_EVENT from homeassistant.components.fan import DOMAIN as DOMAIN_FAN from homeassistant.components.image import DOMAIN as DOMAIN_IMAGE from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT @@ -25,6 +26,7 @@ from homeassistant.components.number import DOMAIN as DOMAIN_NUMBER from homeassistant.components.select import DOMAIN as DOMAIN_SELECT from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH +from homeassistant.components.update import DOMAIN as DOMAIN_UPDATE from homeassistant.components.vacuum import DOMAIN as DOMAIN_VACUUM from homeassistant.components.weather import DOMAIN as DOMAIN_WEATHER from homeassistant.config import async_log_schema_error, config_without_domain @@ -53,6 +55,7 @@ from . import ( binary_sensor as binary_sensor_platform, button as button_platform, cover as cover_platform, + event as event_platform, fan as fan_platform, image as image_platform, light as light_platform, @@ -61,6 +64,7 @@ from . import ( select as select_platform, sensor as sensor_platform, switch as switch_platform, + update as update_platform, vacuum as vacuum_platform, weather as weather_platform, ) @@ -124,6 +128,9 @@ CONFIG_SECTION_SCHEMA = vol.All( vol.Optional(DOMAIN_COVER): vol.All( cv.ensure_list, [cover_platform.COVER_YAML_SCHEMA] ), + vol.Optional(DOMAIN_EVENT): vol.All( + cv.ensure_list, [event_platform.EVENT_YAML_SCHEMA] + ), vol.Optional(DOMAIN_FAN): vol.All( cv.ensure_list, [fan_platform.FAN_YAML_SCHEMA] ), @@ -148,6 +155,9 @@ CONFIG_SECTION_SCHEMA = vol.All( vol.Optional(DOMAIN_SWITCH): vol.All( cv.ensure_list, [switch_platform.SWITCH_YAML_SCHEMA] ), + vol.Optional(DOMAIN_UPDATE): vol.All( + cv.ensure_list, [update_platform.UPDATE_YAML_SCHEMA] + ), vol.Optional(DOMAIN_VACUUM): vol.All( cv.ensure_list, [vacuum_platform.VACUUM_YAML_SCHEMA] ), diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 2e581628da2..36c27aa19f9 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.components import websocket_api from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass from homeassistant.components.cover import CoverDeviceClass +from homeassistant.components.event import EventDeviceClass from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASS_STATE_CLASSES, @@ -19,6 +20,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorStateClass, ) +from homeassistant.components.update import UpdateDeviceClass from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_DEVICE_ID, @@ -72,6 +74,7 @@ from .cover import ( STOP_ACTION, async_create_preview_cover, ) +from .event import CONF_EVENT_TYPE, CONF_EVENT_TYPES, async_create_preview_event from .fan import ( CONF_OFF_ACTION, CONF_ON_ACTION, @@ -104,6 +107,19 @@ from .select import CONF_OPTIONS, CONF_SELECT_OPTION, async_create_preview_selec from .sensor import async_create_preview_sensor from .switch import async_create_preview_switch from .template_entity import TemplateEntity +from .update import ( + CONF_BACKUP, + CONF_IN_PROGRESS, + CONF_INSTALL, + CONF_INSTALLED_VERSION, + CONF_LATEST_VERSION, + CONF_RELEASE_SUMMARY, + CONF_RELEASE_URL, + CONF_SPECIFIC_VERSION, + CONF_TITLE, + CONF_UPDATE_PERCENTAGE, + async_create_preview_update, +) from .vacuum import ( CONF_FAN_SPEED, CONF_FAN_SPEED_LIST, @@ -203,6 +219,24 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: ) } + if domain == Platform.EVENT: + schema |= { + vol.Required(CONF_EVENT_TYPE): selector.TemplateSelector(), + vol.Required(CONF_EVENT_TYPES): selector.TemplateSelector(), + } + + if flow_type == "config": + schema |= { + vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[cls.value for cls in EventDeviceClass], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="event_device_class", + sort=True, + ), + ) + } + if domain == Platform.FAN: schema |= _SCHEMA_STATE | { vol.Required(CONF_ON_ACTION): selector.ActionSelector(), @@ -315,6 +349,31 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_TURN_OFF): selector.ActionSelector(), } + if domain == Platform.UPDATE: + schema |= { + vol.Optional(CONF_INSTALLED_VERSION): selector.TemplateSelector(), + vol.Optional(CONF_LATEST_VERSION): selector.TemplateSelector(), + vol.Optional(CONF_INSTALL): selector.ActionSelector(), + vol.Optional(CONF_IN_PROGRESS): selector.TemplateSelector(), + vol.Optional(CONF_RELEASE_SUMMARY): selector.TemplateSelector(), + vol.Optional(CONF_RELEASE_URL): selector.TemplateSelector(), + vol.Optional(CONF_TITLE): selector.TemplateSelector(), + vol.Optional(CONF_UPDATE_PERCENTAGE): selector.TemplateSelector(), + vol.Optional(CONF_BACKUP): selector.BooleanSelector(), + vol.Optional(CONF_SPECIFIC_VERSION): selector.BooleanSelector(), + } + if flow_type == "config": + schema |= { + vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[cls.value for cls in UpdateDeviceClass], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="update_device_class", + sort=True, + ), + ), + } + if domain == Platform.VACUUM: schema |= _SCHEMA_STATE | { vol.Required(SERVICE_START): selector.ActionSelector(), @@ -441,6 +500,7 @@ TEMPLATE_TYPES = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.IMAGE, Platform.LIGHT, @@ -449,6 +509,7 @@ TEMPLATE_TYPES = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, Platform.VACUUM, ] @@ -473,6 +534,11 @@ CONFIG_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.COVER), ), + Platform.EVENT: SchemaFlowFormStep( + config_schema(Platform.EVENT), + preview="template", + validate_user_input=validate_user_input(Platform.EVENT), + ), Platform.FAN: SchemaFlowFormStep( config_schema(Platform.FAN), preview="template", @@ -513,6 +579,11 @@ CONFIG_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.SWITCH), ), + Platform.UPDATE: SchemaFlowFormStep( + config_schema(Platform.UPDATE), + preview="template", + validate_user_input=validate_user_input(Platform.UPDATE), + ), Platform.VACUUM: SchemaFlowFormStep( config_schema(Platform.VACUUM), preview="template", @@ -542,6 +613,11 @@ OPTIONS_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.COVER), ), + Platform.EVENT: SchemaFlowFormStep( + options_schema(Platform.EVENT), + preview="template", + validate_user_input=validate_user_input(Platform.EVENT), + ), Platform.FAN: SchemaFlowFormStep( options_schema(Platform.FAN), preview="template", @@ -582,6 +658,11 @@ OPTIONS_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.SWITCH), ), + Platform.UPDATE: SchemaFlowFormStep( + options_schema(Platform.UPDATE), + preview="template", + validate_user_input=validate_user_input(Platform.UPDATE), + ), Platform.VACUUM: SchemaFlowFormStep( options_schema(Platform.VACUUM), preview="template", @@ -596,6 +677,7 @@ CREATE_PREVIEW_ENTITY: dict[ Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel, Platform.BINARY_SENSOR: async_create_preview_binary_sensor, Platform.COVER: async_create_preview_cover, + Platform.EVENT: async_create_preview_event, Platform.FAN: async_create_preview_fan, Platform.LIGHT: async_create_preview_light, Platform.LOCK: async_create_preview_lock, @@ -603,6 +685,7 @@ CREATE_PREVIEW_ENTITY: dict[ Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, Platform.SWITCH: async_create_preview_switch, + Platform.UPDATE: async_create_preview_update, Platform.VACUUM: async_create_preview_vacuum, } diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 2180567bf59..5ff2c0137ac 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -38,6 +38,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.IMAGE, Platform.LIGHT, @@ -46,6 +47,7 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, Platform.VACUUM, Platform.WEATHER, ] diff --git a/homeassistant/components/template/entity.py b/homeassistant/components/template/entity.py index 03a93f50ec3..4901a7a7be8 100644 --- a/homeassistant/components/template/entity.py +++ b/homeassistant/components/template/entity.py @@ -34,16 +34,20 @@ class AbstractTemplateEntity(Entity): self._action_scripts: dict[str, Script] = {} if self._optimistic_entity: + optimistic = config.get(CONF_OPTIMISTIC) + self._template = config.get(CONF_STATE) - optimistic = self._template is None + assumed_optimistic = self._template is None if self._extra_optimistic_options: - optimistic = optimistic and all( + assumed_optimistic = assumed_optimistic and all( config.get(option) is None for option in self._extra_optimistic_options ) - self._attr_assumed_state = optimistic or config.get(CONF_OPTIMISTIC, False) + self._attr_assumed_state = optimistic or ( + optimistic is None and assumed_optimistic + ) if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( diff --git a/homeassistant/components/template/event.py b/homeassistant/components/template/event.py new file mode 100644 index 00000000000..358fec6a00f --- /dev/null +++ b/homeassistant/components/template/event.py @@ -0,0 +1,235 @@ +"""Support for events which integrates with other components.""" + +from __future__ import annotations + +import logging +from typing import Any, Final + +import voluptuous as vol + +from homeassistant.components.event import ( + DOMAIN as EVENT_DOMAIN, + ENTITY_ID_FORMAT, + EventDeviceClass, + EventEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_CLASS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import TriggerUpdateCoordinator +from .entity import AbstractTemplateEntity +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, + TemplateEntity, + make_template_entity_common_modern_attributes_schema, +) +from .trigger_entity import TriggerEntity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Template Event" + +CONF_EVENT_TYPE = "event_type" +CONF_EVENT_TYPES = "event_types" + +DEVICE_CLASS_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(EventDeviceClass)) + +EVENT_COMMON_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASS_SCHEMA, + vol.Required(CONF_EVENT_TYPE): cv.template, + vol.Required(CONF_EVENT_TYPES): cv.template, + } +) + +EVENT_YAML_SCHEMA = EVENT_COMMON_SCHEMA.extend( + make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema +) + + +EVENT_CONFIG_ENTRY_SCHEMA = EVENT_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the template event.""" + await async_setup_template_platform( + hass, + EVENT_DOMAIN, + config, + StateEventEntity, + TriggerEventEntity, + async_add_entities, + discovery_info, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateEventEntity, + EVENT_CONFIG_ENTRY_SCHEMA, + True, + ) + + +@callback +def async_create_preview_event( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateEventEntity: + """Create a preview event.""" + return async_setup_template_preview( + hass, + name, + config, + StateEventEntity, + EVENT_CONFIG_ENTRY_SCHEMA, + ) + + +class AbstractTemplateEvent(AbstractTemplateEntity, EventEntity): + """Representation of a template event features.""" + + _entity_id_format = ENTITY_ID_FORMAT + + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" + self._event_type_template = config[CONF_EVENT_TYPE] + self._event_types_template = config[CONF_EVENT_TYPES] + + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + + self._event_type = None + self._attr_event_types = [] + + @callback + def _update_event_types(self, event_types: Any) -> None: + """Update the event types from the template.""" + if event_types in (None, "None", ""): + self._attr_event_types = [] + return + + if not isinstance(event_types, list): + _LOGGER.error( + ("Received invalid event_types list: %s for entity %s. Expected list"), + event_types, + self.entity_id, + ) + self._attr_event_types = [] + return + + self._attr_event_types = [str(event_type) for event_type in event_types] + + @callback + def _update_event_type(self, event_type: Any) -> None: + """Update the effect from the template.""" + try: + self._trigger_event(event_type) + except ValueError: + _LOGGER.error( + "Received invalid event_type: %s for entity %s. Expected one of: %s", + event_type, + self.entity_id, + self._attr_event_types, + ) + + +class StateEventEntity(TemplateEntity, AbstractTemplateEvent): + """Representation of a template event.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: dict[str, Any], + unique_id: str | None, + ) -> None: + """Initialize the select.""" + TemplateEntity.__init__(self, hass, config, unique_id) + AbstractTemplateEvent.__init__(self, config) + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + self.add_template_attribute( + "_attr_event_types", + self._event_types_template, + None, + self._update_event_types, + none_on_template_error=True, + ) + self.add_template_attribute( + "_event_type", + self._event_type_template, + None, + self._update_event_type, + none_on_template_error=True, + ) + super()._async_setup_templates() + + +class TriggerEventEntity(TriggerEntity, AbstractTemplateEvent, RestoreEntity): + """Event entity based on trigger data.""" + + domain = EVENT_DOMAIN + extra_template_keys_complex = ( + CONF_EVENT_TYPE, + CONF_EVENT_TYPES, + ) + + def __init__( + self, + hass: HomeAssistant, + coordinator: TriggerUpdateCoordinator, + config: ConfigType, + ) -> None: + """Initialize the entity.""" + TriggerEntity.__init__(self, hass, coordinator, config) + AbstractTemplateEvent.__init__(self, config) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle update of the data.""" + self._process_data() + + if not self.available: + self.async_write_ha_state() + return + + for key, updater in ( + (CONF_EVENT_TYPES, self._update_event_types), + (CONF_EVENT_TYPE, self._update_event_type), + ): + updater(self._rendered[key]) + + self.async_set_context(self.coordinator.data["context"]) + self.async_write_ha_state() diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 200b323d377..5b62f6bc8e8 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -136,6 +136,32 @@ }, "title": "Template cover" }, + "event": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "device_class": "[%key:component::template::common::device_class%]", + "event_type": "Last fired event type", + "event_types": "[%key:component::event::entity_component::_::state_attributes::event_types::name%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "event_type": "Defines a template for the type of the event.", + "event_types": "Defines a template for a list of available event types." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template event" + }, "fan": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -216,7 +242,7 @@ "level": "Defines a template to get the brightness of the light. Valid values are 0 to 255.", "set_level": "Defines actions to run when the light is given a brightness command. The script will only be called if the `turn_on` call only has `brightness`, and optionally `transition`. Receives variables `brightness` and, optionally, `transition`.", "hs": "Defines a template to get the HS color of the light. Must render a tuple (hue, saturation).", - "set_hs": "Defines actions to run when the light is given a hs color command. Available variables: `hs` as a tuple, `h` and `s`.", + "set_hs": "Defines actions to run when the light is given an HS color command. Available variables: `hs` as a tuple, `h` and `s`.", "temperature": "Defines a template to get the color temperature of the light.", "set_temperature": "Defines actions to run when the light is given a color temperature command. Receives variable `color_temp_kelvin`. May also receive variables `brightness` and/or `transition`." }, @@ -358,6 +384,7 @@ "binary_sensor": "Template a binary sensor", "button": "Template a button", "cover": "Template a cover", + "event": "Template an event", "fan": "Template a fan", "image": "Template an image", "light": "Template a light", @@ -366,6 +393,7 @@ "select": "Template a select", "sensor": "Template a sensor", "switch": "Template a switch", + "update": "Template an update", "vacuum": "Template a vacuum" }, "title": "Template helper" @@ -397,6 +425,48 @@ }, "title": "Template switch" }, + "update": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "device_class": "[%key:component::template::common::device_class%]", + "name": "[%key:common::config_flow::data::name%]", + "installed_version": "[%key:component::update::entity_component::_::state_attributes::installed_version::name%]", + "latest_version": "[%key:component::update::entity_component::_::state_attributes::latest_version::name%]", + "install": "Actions on install", + "in_progress": "[%key:component::update::entity_component::_::state_attributes::in_progress::name%]", + "release_summary": "[%key:component::update::entity_component::_::state_attributes::release_summary::name%]", + "release_url": "[%key:component::update::entity_component::_::state_attributes::release_url::name%]", + "title": "[%key:component::update::entity_component::_::state_attributes::title::name%]", + "backup": "Backup", + "specific_version": "Specific version", + "update_percent": "Update percentage" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "installed_version": "Defines a template to get the installed version.", + "latest_version": "Defines a template to get the latest version.", + "install": "Defines actions to run when the update is installed. Receives variables `specific_version` and `backup` when enabled.", + "in_progress": "Defines a template to get the in-progress state.", + "release_summary": "Defines a template to get the release summary.", + "release_url": "Defines a template to get the release URL.", + "title": "Defines a template to get the update title.", + "backup": "Enable or disable the `automatic backup before update` option in the update repair. When disabled, the `backup` variable will always provide `False` during the `install` action and it will not accept the `backup` option.", + "specific_version": "Enable or disable using the `version` variable with the `install` action. When disabled, the `specific_version` variable will always provide `None` in the `install` actions", + "update_percent": "Defines a template to get the update completion percentage." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template update" + }, "vacuum": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -565,6 +635,31 @@ }, "title": "[%key:component::template::config::step::cover::title%]" }, + "event": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "event_type": "[%key:component::template::config::step::event::data::event_type%]", + "event_types": "[%key:component::event::entity_component::_::state_attributes::event_types::name%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "event_type": "[%key:component::template::config::step::event::data_description::event_type%]", + "event_types": "[%key:component::template::config::step::event::data_description::event_types%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "[%key:component::template::config::step::event::title%]" + }, "fan": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -801,6 +896,48 @@ }, "title": "[%key:component::template::config::step::switch::title%]" }, + "update": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "device_class": "[%key:component::template::common::device_class%]", + "name": "[%key:common::config_flow::data::name%]", + "installed_version": "[%key:component::update::entity_component::_::state_attributes::installed_version::name%]", + "latest_version": "[%key:component::update::entity_component::_::state_attributes::latest_version::name%]", + "install": "[%key:component::template::config::step::update::data::install%]", + "in_progress": "[%key:component::update::entity_component::_::state_attributes::in_progress::name%]", + "release_summary": "[%key:component::update::entity_component::_::state_attributes::release_summary::name%]", + "release_url": "[%key:component::update::entity_component::_::state_attributes::release_url::name%]", + "title": "[%key:component::update::entity_component::_::state_attributes::title::name%]", + "backup": "[%key:component::template::config::step::update::data::backup%]", + "specific_version": "[%key:component::template::config::step::update::data::specific_version%]", + "update_percent": "[%key:component::template::config::step::update::data::update_percent%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "installed_version": "[%key:component::template::config::step::update::data_description::installed_version%]", + "latest_version": "[%key:component::template::config::step::update::data_description::latest_version%]", + "install": "[%key:component::template::config::step::update::data_description::install%]", + "in_progress": "[%key:component::template::config::step::update::data_description::in_progress%]", + "release_summary": "[%key:component::template::config::step::update::data_description::release_summary%]", + "release_url": "[%key:component::template::config::step::update::data_description::release_url%]", + "title": "[%key:component::template::config::step::update::data_description::title%]", + "backup": "[%key:component::template::config::step::update::data_description::backup%]", + "specific_version": "[%key:component::template::config::step::update::data_description::specific_version%]", + "update_percent": "[%key:component::template::config::step::update::data_description::update_percent%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template update" + }, "vacuum": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -905,6 +1042,13 @@ "window": "[%key:component::cover::entity_component::window::name%]" } }, + "event_device_class": { + "options": { + "doorbell": "[%key:component::event::entity_component::doorbell::name%]", + "button": "[%key:component::event::entity_component::button::name%]", + "motion": "[%key:component::event::entity_component::motion::name%]" + } + }, "sensor_device_class": { "options": { "absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]", @@ -978,6 +1122,11 @@ "options": { "none": "No unit of measurement" } + }, + "update_device_class": { + "options": { + "firmware": "[%key:component::update::entity_component::firmware::name%]" + } } }, "services": { diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 1bc49bceafd..3ba89cae1f4 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -102,7 +102,7 @@ TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA = vol.Schema( TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA = { - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_OPTIMISTIC): cv.boolean, } diff --git a/homeassistant/components/template/update.py b/homeassistant/components/template/update.py new file mode 100644 index 00000000000..a6b0bca0f5f --- /dev/null +++ b/homeassistant/components/template/update.py @@ -0,0 +1,463 @@ +"""Support for updates which integrates with other components.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +import voluptuous as vol + +from homeassistant.components.update import ( + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + DEVICE_CLASSES_SCHEMA, + DOMAIN as UPDATE_DOMAIN, + ENTITY_ID_FORMAT, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) +from homeassistant.helpers.template import _SENTINEL +from homeassistant.helpers.trigger_template_entity import CONF_PICTURE +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import TriggerUpdateCoordinator +from .const import DOMAIN +from .entity import AbstractTemplateEntity +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, + TemplateEntity, + make_template_entity_common_modern_schema, +) +from .trigger_entity import TriggerEntity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Template Update" + +ATTR_BACKUP = "backup" +ATTR_SPECIFIC_VERSION = "specific_version" + +CONF_BACKUP = "backup" +CONF_IN_PROGRESS = "in_progress" +CONF_INSTALL = "install" +CONF_INSTALLED_VERSION = "installed_version" +CONF_LATEST_VERSION = "latest_version" +CONF_RELEASE_SUMMARY = "release_summary" +CONF_RELEASE_URL = "release_url" +CONF_SPECIFIC_VERSION = "specific_version" +CONF_TITLE = "title" +CONF_UPDATE_PERCENTAGE = "update_percentage" + +UPDATE_COMMON_SCHEMA = vol.Schema( + { + vol.Optional(CONF_BACKUP, default=False): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_IN_PROGRESS): cv.template, + vol.Optional(CONF_INSTALL): cv.SCRIPT_SCHEMA, + vol.Required(CONF_INSTALLED_VERSION): cv.template, + vol.Required(CONF_LATEST_VERSION): cv.template, + vol.Optional(CONF_RELEASE_SUMMARY): cv.template, + vol.Optional(CONF_RELEASE_URL): cv.template, + vol.Optional(CONF_SPECIFIC_VERSION, default=False): cv.boolean, + vol.Optional(CONF_TITLE): cv.template, + vol.Optional(CONF_UPDATE_PERCENTAGE): cv.template, + } +) + +UPDATE_YAML_SCHEMA = UPDATE_COMMON_SCHEMA.extend( + make_template_entity_common_modern_schema(DEFAULT_NAME).schema +) + +UPDATE_CONFIG_ENTRY_SCHEMA = UPDATE_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Template update.""" + await async_setup_template_platform( + hass, + UPDATE_DOMAIN, + config, + StateUpdateEntity, + TriggerUpdateEntity, + async_add_entities, + discovery_info, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateUpdateEntity, + UPDATE_CONFIG_ENTRY_SCHEMA, + ) + + +@callback +def async_create_preview_update( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateUpdateEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateUpdateEntity, + UPDATE_CONFIG_ENTRY_SCHEMA, + ) + + +class AbstractTemplateUpdate(AbstractTemplateEntity, UpdateEntity): + """Representation of a template update features.""" + + _entity_id_format = ENTITY_ID_FORMAT + + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" + + self._installed_version_template = config[CONF_INSTALLED_VERSION] + self._latest_version_template = config[CONF_LATEST_VERSION] + + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + + self._in_progress_template = config.get(CONF_IN_PROGRESS) + self._release_summary_template = config.get(CONF_RELEASE_SUMMARY) + self._release_url_template = config.get(CONF_RELEASE_URL) + self._title_template = config.get(CONF_TITLE) + self._update_percentage_template = config.get(CONF_UPDATE_PERCENTAGE) + + self._attr_supported_features = UpdateEntityFeature(0) + if config[CONF_BACKUP]: + self._attr_supported_features |= UpdateEntityFeature.BACKUP + if config[CONF_SPECIFIC_VERSION]: + self._attr_supported_features |= UpdateEntityFeature.SPECIFIC_VERSION + if ( + self._in_progress_template is not None + or self._update_percentage_template is not None + ): + self._attr_supported_features |= UpdateEntityFeature.PROGRESS + + self._optimistic_in_process = ( + self._in_progress_template is None + and self._update_percentage_template is not None + ) + + @callback + def _update_installed_version(self, result: Any) -> None: + if result is None: + self._attr_installed_version = None + return + + self._attr_installed_version = cv.string(result) + + @callback + def _update_latest_version(self, result: Any) -> None: + if result is None: + self._attr_latest_version = None + return + + self._attr_latest_version = cv.string(result) + + @callback + def _update_in_process(self, result: Any) -> None: + try: + self._attr_in_progress = cv.boolean(result) + except vol.Invalid: + _LOGGER.error( + "Received invalid in_process value: %s for entity %s. Expected: True, False", + result, + self.entity_id, + ) + self._attr_in_progress = False + + @callback + def _update_release_summary(self, result: Any) -> None: + if result is None: + self._attr_release_summary = None + return + + self._attr_release_summary = cv.string(result) + + @callback + def _update_release_url(self, result: Any) -> None: + if result is None: + self._attr_release_url = None + return + + try: + self._attr_release_url = cv.url(result) + except vol.Invalid: + _LOGGER.error( + "Received invalid release_url: %s for entity %s", + result, + self.entity_id, + ) + self._attr_release_url = None + + @callback + def _update_title(self, result: Any) -> None: + if result is None: + self._attr_title = None + return + + self._attr_title = cv.string(result) + + @callback + def _update_update_percentage(self, result: Any) -> None: + if result is None: + if self._optimistic_in_process: + self._attr_in_progress = False + self._attr_update_percentage = None + return + + try: + percentage = vol.All( + vol.Coerce(float), + vol.Range(0, 100, min_included=True, max_included=True), + )(result) + if self._optimistic_in_process: + self._attr_in_progress = True + self._attr_update_percentage = percentage + except vol.Invalid: + _LOGGER.error( + "Received invalid update_percentage: %s for entity %s", + result, + self.entity_id, + ) + self._attr_update_percentage = None + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + await self.async_run_script( + self._action_scripts[CONF_INSTALL], + run_variables={ATTR_SPECIFIC_VERSION: version, ATTR_BACKUP: backup}, + context=self._context, + ) + + +class StateUpdateEntity(TemplateEntity, AbstractTemplateUpdate): + """Representation of a Template update.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + unique_id: str | None, + ) -> None: + """Initialize the Template update.""" + TemplateEntity.__init__(self, hass, config, unique_id) + AbstractTemplateUpdate.__init__(self, config) + + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + # Scripts can be an empty list, therefore we need to check for None + if (install_action := config.get(CONF_INSTALL)) is not None: + self.add_script(CONF_INSTALL, install_action, name, DOMAIN) + self._attr_supported_features |= UpdateEntityFeature.INSTALL + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend.""" + # This is needed to override the base update entity functionality + if self._attr_entity_picture is None: + # The default picture for update entities would use `self.platform.platform_name` in + # place of `template`. This does not work when creating an entity preview because + # the platform does not exist for that entity, therefore this is hardcoded as `template`. + return "https://brands.home-assistant.io/_/template/icon.png" + return self._attr_entity_picture + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + self.add_template_attribute( + "_attr_installed_version", + self._installed_version_template, + None, + self._update_installed_version, + none_on_template_error=True, + ) + self.add_template_attribute( + "_attr_latest_version", + self._latest_version_template, + None, + self._update_latest_version, + none_on_template_error=True, + ) + if self._in_progress_template is not None: + self.add_template_attribute( + "_attr_in_progress", + self._in_progress_template, + None, + self._update_in_process, + none_on_template_error=True, + ) + if self._release_summary_template is not None: + self.add_template_attribute( + "_attr_release_summary", + self._release_summary_template, + None, + self._update_release_summary, + none_on_template_error=True, + ) + if self._release_url_template is not None: + self.add_template_attribute( + "_attr_release_url", + self._release_url_template, + None, + self._update_release_url, + none_on_template_error=True, + ) + if self._title_template is not None: + self.add_template_attribute( + "_attr_title", + self._title_template, + None, + self._update_title, + none_on_template_error=True, + ) + if self._update_percentage_template is not None: + self.add_template_attribute( + "_attr_update_percentage", + self._update_percentage_template, + None, + self._update_update_percentage, + none_on_template_error=True, + ) + super()._async_setup_templates() + + +class TriggerUpdateEntity(TriggerEntity, AbstractTemplateUpdate): + """Update entity based on trigger data.""" + + domain = UPDATE_DOMAIN + + def __init__( + self, + hass: HomeAssistant, + coordinator: TriggerUpdateCoordinator, + config: ConfigType, + ) -> None: + """Initialize the entity.""" + TriggerEntity.__init__(self, hass, coordinator, config) + AbstractTemplateUpdate.__init__(self, config) + + for key in ( + CONF_INSTALLED_VERSION, + CONF_LATEST_VERSION, + ): + self._to_render_simple.append(key) + self._parse_result.add(key) + + # Scripts can be an empty list, therefore we need to check for None + if (install_action := config.get(CONF_INSTALL)) is not None: + self.add_script( + CONF_INSTALL, + install_action, + self._rendered.get(CONF_NAME, DEFAULT_NAME), + DOMAIN, + ) + self._attr_supported_features |= UpdateEntityFeature.INSTALL + + for key in ( + CONF_IN_PROGRESS, + CONF_RELEASE_SUMMARY, + CONF_RELEASE_URL, + CONF_TITLE, + CONF_UPDATE_PERCENTAGE, + ): + if isinstance(config.get(key), template.Template): + self._to_render_simple.append(key) + self._parse_result.add(key) + + # Ensure the entity picture can resolve None to produce the default picture. + if CONF_PICTURE in config: + self._parse_result.add(CONF_PICTURE) + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if ( + (last_state := await self.async_get_last_state()) is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + and self._attr_installed_version is None + and self._attr_latest_version is None + ): + self._attr_installed_version = last_state.attributes[ATTR_INSTALLED_VERSION] + self._attr_latest_version = last_state.attributes[ATTR_LATEST_VERSION] + self.restore_attributes(last_state) + + @property + def entity_picture(self) -> str | None: + """Return entity picture.""" + if (picture := self._rendered.get(CONF_PICTURE)) is None: + return UpdateEntity.entity_picture.fget(self) # type: ignore[attr-defined] + return picture + + @callback + def _handle_coordinator_update(self) -> None: + """Handle update of the data.""" + self._process_data() + + if not self.available: + self.async_write_ha_state() + return + + write_ha_state = False + for key, updater in ( + (CONF_INSTALLED_VERSION, self._update_installed_version), + (CONF_LATEST_VERSION, self._update_latest_version), + (CONF_IN_PROGRESS, self._update_in_process), + (CONF_RELEASE_SUMMARY, self._update_release_summary), + (CONF_RELEASE_URL, self._update_release_url), + (CONF_TITLE, self._update_title), + (CONF_UPDATE_PERCENTAGE, self._update_update_percentage), + ): + if (rendered := self._rendered.get(key, _SENTINEL)) is not _SENTINEL: + updater(rendered) + write_ha_state = True + + if len(self._rendered) > 0: + # In case any non optimistic template + write_ha_state = True + + if write_ha_state: + self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 688a254a731..af4ce26a0cc 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -97,6 +97,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - # Create the stream stream: TeslemetryStream | None = None + # Remember each device identifier we create + current_devices: set[tuple[str, str]] = set() + for product in products: if ( "vin" in product @@ -116,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - model=api.model, serial_number=vin, ) + current_devices.add((DOMAIN, vin)) # Create stream if required if not stream: @@ -171,6 +175,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - name=product.get("site_name", "Energy Site"), serial_number=str(site_id), ) + current_devices.add((DOMAIN, str(site_id))) + + if wall_connector: + for connector in product["components"]["wall_connectors"]: + current_devices.add((DOMAIN, connector["din"])) # Check live status endpoint works before creating its coordinator try: @@ -235,6 +244,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - config_entry_id=entry.entry_id, **energysite.device ) + # Remove devices that are no longer present + for device_entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ): + if not any( + identifier in current_devices for identifier in device_entry.identifiers + ): + LOGGER.debug("Removing stale device %s", device_entry.id) + device_registry.async_update_device( + device_id=device_entry.id, + remove_config_entry_id=entry.entry_id, + ) + # Setup Platforms entry.runtime_data = TeslemetryData(vehicles, energysites, scopes, stream) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index edd5d404499..f50f5a75f70 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -773,6 +773,18 @@ }, "time_of_use": { "service": "mdi:clock-time-eight-outline" + }, + "add_charge_schedule": { + "service": "mdi:calendar-plus" + }, + "remove_charge_schedule": { + "service": "mdi:calendar-minus" + }, + "add_precondition_schedule": { + "service": "mdi:hvac-outline" + }, + "remove_precondition_schedule": { + "service": "mdi:hvac-off-outline" } } } diff --git a/homeassistant/components/teslemetry/services.py b/homeassistant/components/teslemetry/services.py index 246cc097a2a..7a6a7b55c0c 100644 --- a/homeassistant/components/teslemetry/services.py +++ b/homeassistant/components/teslemetry/services.py @@ -22,6 +22,7 @@ ATTR_ID = "id" ATTR_GPS = "gps" ATTR_TYPE = "type" ATTR_VALUE = "value" +ATTR_LOCATION = "location" ATTR_LOCALE = "locale" ATTR_ORDER = "order" ATTR_TIMESTAMP = "timestamp" @@ -36,6 +37,12 @@ ATTR_DEPARTURE_TIME = "departure_time" ATTR_OFF_PEAK_CHARGING_ENABLED = "off_peak_charging_enabled" ATTR_OFF_PEAK_CHARGING_WEEKDAYS = "off_peak_charging_weekdays_only" ATTR_END_OFF_PEAK_TIME = "end_off_peak_time" +ATTR_DAYS_OF_WEEK = "days_of_week" +ATTR_START_TIME = "start_time" +ATTR_END_TIME = "end_time" +ATTR_ONE_TIME = "one_time" +ATTR_NAME = "name" +ATTR_PRECONDITION_TIME = "precondition_time" # Services SERVICE_NAVIGATE_ATTR_GPS_REQUEST = "navigation_gps_request" @@ -44,6 +51,10 @@ SERVICE_SET_SCHEDULED_DEPARTURE = "set_scheduled_departure" SERVICE_VALET_MODE = "valet_mode" SERVICE_SPEED_LIMIT = "speed_limit" SERVICE_TIME_OF_USE = "time_of_use" +SERVICE_ADD_CHARGE_SCHEDULE = "add_charge_schedule" +SERVICE_REMOVE_CHARGE_SCHEDULE = "remove_charge_schedule" +SERVICE_ADD_PRECONDITION_SCHEDULE = "add_precondition_schedule" +SERVICE_REMOVE_PRECONDITION_SCHEDULE = "remove_precondition_schedule" def async_get_device_for_service_call( @@ -315,3 +326,195 @@ def async_setup_services(hass: HomeAssistant) -> None: } ), ) + + async def add_charge_schedule(call: ServiceCall) -> None: + """Configure charging schedule for a vehicle.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + # Extract parameters from the service call + days_of_week = call.data[ATTR_DAYS_OF_WEEK] + # If days_of_week is a list (from select with multiple), convert to comma-separated string + if isinstance(days_of_week, list): + days_of_week = ",".join(days_of_week) + enabled = call.data[ATTR_ENABLE] + + # Optional parameters + location = call.data.get( + ATTR_LOCATION, + { + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + }, + ) + + # Handle time inputs + start_time = None + if start_time_obj := call.data.get(ATTR_START_TIME): + # Convert time object to minutes since midnight + start_time = start_time_obj.hour * 60 + start_time_obj.minute + + end_time = None + if end_time_obj := call.data.get(ATTR_END_TIME): + # Convert time object to minutes since midnight + end_time = end_time_obj.hour * 60 + end_time_obj.minute + + one_time = call.data.get(ATTR_ONE_TIME) + schedule_id = call.data.get(ATTR_ID) + name = call.data.get(ATTR_NAME) + + await handle_vehicle_command( + vehicle.api.add_charge_schedule( + days_of_week=days_of_week, + enabled=enabled, + lat=location[CONF_LATITUDE], + lon=location[CONF_LONGITUDE], + start_time=start_time, + end_time=end_time, + one_time=one_time, + id=schedule_id, + name=name, + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ADD_CHARGE_SCHEDULE, + add_charge_schedule, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_DAYS_OF_WEEK): cv.ensure_list, + vol.Required(ATTR_ENABLE): cv.boolean, + vol.Optional(ATTR_LOCATION): { + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + }, + vol.Optional(ATTR_START_TIME): cv.time, + vol.Optional(ATTR_END_TIME): cv.time, + vol.Optional(ATTR_ONE_TIME): cv.boolean, + vol.Optional(ATTR_ID): cv.positive_int, + vol.Optional(ATTR_NAME): cv.string, + } + ), + ) + + async def remove_charge_schedule(call: ServiceCall) -> None: + """Remove a charging schedule for a vehicle.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + # Extract parameters from the service call + schedule_id = call.data[ATTR_ID] + + await handle_vehicle_command( + vehicle.api.remove_charge_schedule( + id=schedule_id, + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_REMOVE_CHARGE_SCHEDULE, + remove_charge_schedule, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ID): cv.positive_int, + } + ), + ) + + async def add_precondition_schedule(call: ServiceCall) -> None: + """Add or modify a precondition schedule for a vehicle.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + # Extract parameters from the service call + days_of_week = call.data[ATTR_DAYS_OF_WEEK] + # If days_of_week is a list (from select with multiple), convert to comma-separated string + if isinstance(days_of_week, list): + days_of_week = ",".join(days_of_week) + enabled = call.data[ATTR_ENABLE] + location = call.data.get( + ATTR_LOCATION, + { + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + }, + ) + + # Convert time object to minutes since midnight + precondition_time = ( + call.data[ATTR_PRECONDITION_TIME].hour * 60 + + call.data[ATTR_PRECONDITION_TIME].minute + ) + + # Optional parameters + schedule_id = call.data.get(ATTR_ID) + one_time = call.data.get(ATTR_ONE_TIME) + name = call.data.get(ATTR_NAME) + + await handle_vehicle_command( + vehicle.api.add_precondition_schedule( + days_of_week=days_of_week, + enabled=enabled, + lat=location[CONF_LATITUDE], + lon=location[CONF_LONGITUDE], + precondition_time=precondition_time, + id=schedule_id, + one_time=one_time, + name=name, + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ADD_PRECONDITION_SCHEDULE, + add_precondition_schedule, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_DAYS_OF_WEEK): cv.ensure_list, + vol.Required(ATTR_ENABLE): cv.boolean, + vol.Optional(ATTR_LOCATION): { + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + }, + vol.Required(ATTR_PRECONDITION_TIME): cv.time, + vol.Optional(ATTR_ID): cv.positive_int, + vol.Optional(ATTR_ONE_TIME): cv.boolean, + vol.Optional(ATTR_NAME): cv.string, + } + ), + ) + + async def remove_precondition_schedule(call: ServiceCall) -> None: + """Remove a preconditioning schedule for a vehicle.""" + device = async_get_device_for_service_call(hass, call) + config = async_get_config_for_device(hass, device) + vehicle = async_get_vehicle_for_entry(hass, device, config) + + # Extract parameters from the service call + schedule_id = call.data[ATTR_ID] + + await handle_vehicle_command( + vehicle.api.remove_precondition_schedule( + id=schedule_id, + ) + ) + + hass.services.async_register( + DOMAIN, + SERVICE_REMOVE_PRECONDITION_SCHEDULE, + remove_precondition_schedule, + schema=vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(ATTR_ID): cv.positive_int, + } + ), + ) diff --git a/homeassistant/components/teslemetry/services.yaml b/homeassistant/components/teslemetry/services.yaml index e98f124dd19..4c941c5d41d 100644 --- a/homeassistant/components/teslemetry/services.yaml +++ b/homeassistant/components/teslemetry/services.yaml @@ -130,3 +130,139 @@ speed_limit: min: 1000 max: 9999 mode: box + +add_charge_schedule: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + days_of_week: + required: true + selector: + select: + options: + - monday + - tuesday + - wednesday + - thursday + - friday + - saturday + - sunday + multiple: true + translation_key: days_of_week + enable: + required: true + selector: + boolean: + location: + required: false + example: '{"latitude": -27.9699373, "longitude": 153.4081865}' + selector: + location: + radius: false + start_time: + required: false + selector: + time: + end_time: + required: false + selector: + time: + one_time: + required: false + selector: + boolean: + id: + required: false + selector: + number: + min: 1 + mode: box + name: + required: false + selector: + text: + +remove_charge_schedule: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + id: + required: true + selector: + number: + min: 1 + mode: box + +add_precondition_schedule: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + days_of_week: + required: true + selector: + select: + options: + - monday + - tuesday + - wednesday + - thursday + - friday + - saturday + - sunday + multiple: true + translation_key: days_of_week + enable: + required: true + selector: + boolean: + location: + required: false + example: '{"latitude": -27.9699373, "longitude": 153.4081865}' + selector: + location: + radius: false + precondition_time: + required: true + selector: + time: + id: + required: false + selector: + number: + min: 1 + mode: box + one_time: + required: false + selector: + boolean: + name: + required: false + selector: + text: + +remove_precondition_schedule: + fields: + device_id: + required: true + selector: + device: + filter: + integration: teslemetry + id: + required: true + selector: + number: + min: 1 + mode: box diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 646a3898cc7..510e2b45a02 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -3,6 +3,26 @@ "unavailable": "Unavailable", "abort": "Abort", "vehicle": "Vehicle", + "wake_up_failed": "Failed to wake up vehicle: {message}", + "wake_up_timeout": "Timed out trying to wake up vehicle", + "schedule_id": "Schedule ID", + "schedule_id_description": "The ID of the schedule, use an existing ID to modify.", + "days_of_week": "Days of week", + "days_of_week_description": "Select which days this schedule should be enabled on. You can select multiple days.", + "one_time": "One-time", + "one_time_description": "If this is a one-time schedule.", + "location_description": "The approximate location the vehicle must be at to use this schedule. Defaults to Home Assistant's configured location.", + "start_time": "Start time", + "start_time_description": "The time this schedule begins, e.g. 01:05 for 1:05 AM.", + "end_time": "End time", + "end_time_description": "The time this schedule ends, e.g. 01:05 for 1:05 AM.", + "precondition_time": "Precondition time", + "precondition_time_description": "The time the vehicle should complete preconditioning, e.g. 01:05 for 1:05 AM.", + "schedule_name_description": "The name of the schedule.", + "vehicle_to_schedule": "Vehicle to schedule.", + "vehicle_to_remove_schedule": "Vehicle to remove schedule from.", + "schedule_enable_description": "If this schedule should be considered for execution.", + "schedule_id_remove_description": "The ID of the schedule to remove.", "descr_pin": "4-digit code to enable or disable the setting" }, "config": { @@ -1079,15 +1099,6 @@ "invalid_cop_temp": { "message": "Cabin overheat protection does not support that temperature" }, - "set_scheduled_charging_time": { - "message": "Time required to complete the operation" - }, - "set_scheduled_departure_preconditioning": { - "message": "Departure time required to enable preconditioning" - }, - "set_scheduled_departure_off_peak": { - "message": "To enable scheduled departure, 'End off-peak time' is required." - }, "invalid_device": { "message": "Invalid device ID: {device_id}" }, @@ -1145,7 +1156,7 @@ "description": "Sets a time at which charging should be started.", "fields": { "device_id": { - "description": "Vehicle to schedule.", + "description": "[%key:component::teslemetry::common::vehicle_to_schedule%]", "name": "[%key:component::teslemetry::common::vehicle%]" }, "enable": { @@ -1167,7 +1178,7 @@ "name": "Departure time" }, "device_id": { - "description": "Vehicle to schedule.", + "description": "[%key:component::teslemetry::common::vehicle_to_schedule%]", "name": "[%key:component::teslemetry::common::vehicle%]" }, "enable": { @@ -1246,6 +1257,127 @@ } }, "name": "Set valet mode" + }, + "add_charge_schedule": { + "description": "Adds or modifies a charging schedule for a vehicle.", + "fields": { + "device_id": { + "description": "[%key:component::teslemetry::common::vehicle_to_schedule%]", + "name": "[%key:component::teslemetry::common::vehicle%]" + }, + "days_of_week": { + "description": "[%key:component::teslemetry::common::days_of_week_description%]", + "name": "[%key:component::teslemetry::common::days_of_week%]" + }, + "enable": { + "description": "[%key:component::teslemetry::common::schedule_enable_description%]", + "name": "[%key:common::action::enable%]" + }, + "location": { + "description": "[%key:component::teslemetry::common::location_description%]", + "name": "Location" + }, + "start_time": { + "description": "[%key:component::teslemetry::common::start_time_description%]", + "name": "[%key:component::teslemetry::common::start_time%]" + }, + "end_time": { + "description": "[%key:component::teslemetry::common::end_time_description%]", + "name": "[%key:component::teslemetry::common::end_time%]" + }, + "one_time": { + "description": "[%key:component::teslemetry::common::one_time_description%]", + "name": "[%key:component::teslemetry::common::one_time%]" + }, + "id": { + "description": "[%key:component::teslemetry::common::schedule_id_description%]", + "name": "[%key:component::teslemetry::common::schedule_id%]" + }, + "name": { + "description": "[%key:component::teslemetry::common::schedule_name_description%]", + "name": "[%key:common::config_flow::data::name%]" + } + }, + "name": "Add charge schedule" + }, + "remove_charge_schedule": { + "description": "Removes a charging schedule for a vehicle.", + "fields": { + "device_id": { + "description": "[%key:component::teslemetry::common::vehicle_to_remove_schedule%]", + "name": "[%key:component::teslemetry::common::vehicle%]" + }, + "id": { + "description": "[%key:component::teslemetry::common::schedule_id_remove_description%]", + "name": "[%key:component::teslemetry::common::schedule_id%]" + } + }, + "name": "Remove charge schedule" + }, + "add_precondition_schedule": { + "description": "Adds or modifies a preconditioning schedule for a vehicle.", + "fields": { + "device_id": { + "description": "[%key:component::teslemetry::common::vehicle_to_schedule%]", + "name": "[%key:component::teslemetry::common::vehicle%]" + }, + "days_of_week": { + "description": "[%key:component::teslemetry::common::days_of_week_description%]", + "name": "[%key:component::teslemetry::common::days_of_week%]" + }, + "enable": { + "description": "[%key:component::teslemetry::common::schedule_enable_description%]", + "name": "[%key:common::action::enable%]" + }, + "location": { + "description": "[%key:component::teslemetry::common::location_description%]", + "name": "Location" + }, + "precondition_time": { + "description": "[%key:component::teslemetry::common::precondition_time_description%]", + "name": "[%key:component::teslemetry::common::precondition_time%]" + }, + "id": { + "description": "[%key:component::teslemetry::common::schedule_id_description%]", + "name": "[%key:component::teslemetry::common::schedule_id%]" + }, + "one_time": { + "description": "[%key:component::teslemetry::common::one_time_description%]", + "name": "[%key:component::teslemetry::common::one_time%]" + }, + "name": { + "description": "[%key:component::teslemetry::common::schedule_name_description%]", + "name": "[%key:common::config_flow::data::name%]" + } + }, + "name": "Add precondition schedule" + }, + "remove_precondition_schedule": { + "description": "Removes a preconditioning schedule for a vehicle.", + "fields": { + "device_id": { + "description": "[%key:component::teslemetry::common::vehicle_to_remove_schedule%]", + "name": "[%key:component::teslemetry::common::vehicle%]" + }, + "id": { + "description": "[%key:component::teslemetry::common::schedule_id_remove_description%]", + "name": "[%key:component::teslemetry::common::schedule_id%]" + } + }, + "name": "Remove precondition schedule" + } + }, + "selector": { + "days_of_week": { + "options": { + "monday": "[%key:common::time::monday%]", + "tuesday": "[%key:common::time::tuesday%]", + "wednesday": "[%key:common::time::wednesday%]", + "thursday": "[%key:common::time::thursday%]", + "friday": "[%key:common::time::friday%]", + "saturday": "[%key:common::time::saturday%]", + "sunday": "[%key:common::time::sunday%]" + } } } } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 327812cdf99..1c56d5b2ce6 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -299,7 +299,10 @@ async def async_setup_entry( ) await home.rt_subscribe( TibberRtDataCoordinator( - entity_creator.add_sensors, home, hass + hass, + entry, + entity_creator.add_sensors, + home, ).async_set_updated_data ) @@ -613,15 +616,17 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-en def __init__( self, + hass: HomeAssistant, + config_entry: ConfigEntry, add_sensor_callback: Callable[[TibberRtDataCoordinator, Any], None], tibber_home: tibber.TibberHome, - hass: HomeAssistant, ) -> None: """Initialize the data handler.""" self._add_sensor_callback = add_sensor_callback super().__init__( hass, _LOGGER, + config_entry=config_entry, name=tibber_home.info["viewer"]["home"]["address"].get( "address1", "Tibber" ), diff --git a/homeassistant/components/tilt_ble/manifest.json b/homeassistant/components/tilt_ble/manifest.json index e22c9d5a1d5..1b178cdb2a6 100644 --- a/homeassistant/components/tilt_ble/manifest.json +++ b/homeassistant/components/tilt_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/tilt_ble", "iot_class": "local_push", - "requirements": ["tilt-ble==0.2.3"] + "requirements": ["tilt-ble==0.3.1"] } diff --git a/homeassistant/components/togrill/__init__.py b/homeassistant/components/togrill/__init__.py new file mode 100644 index 00000000000..696b7395f1e --- /dev/null +++ b/homeassistant/components/togrill/__init__.py @@ -0,0 +1,33 @@ +"""The ToGrill integration.""" + +from __future__ import annotations + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .coordinator import DeviceNotFound, ToGrillConfigEntry, ToGrillCoordinator + +_PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.NUMBER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool: + """Set up ToGrill Bluetooth from a config entry.""" + + coordinator = ToGrillCoordinator(hass, entry) + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady as exc: + if not isinstance(exc.__cause__, DeviceNotFound): + raise + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS) diff --git a/homeassistant/components/togrill/config_flow.py b/homeassistant/components/togrill/config_flow.py new file mode 100644 index 00000000000..29d930e7961 --- /dev/null +++ b/homeassistant/components/togrill/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for the ToGrill integration.""" + +from __future__ import annotations + +from typing import Any + +from bleak.exc import BleakError +from togrill_bluetooth import SUPPORTED_DEVICES +from togrill_bluetooth.client import Client +from togrill_bluetooth.packets import PacketA0Notify +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ADDRESS, CONF_MODEL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import AbortFlow + +from .const import CONF_PROBE_COUNT, DOMAIN +from .coordinator import LOGGER + +_TIMEOUT = 10 + + +async def read_config_data( + hass: HomeAssistant, info: BluetoothServiceInfoBleak +) -> dict[str, Any]: + """Read config from device.""" + + try: + client = await Client.connect(info.device) + except BleakError as exc: + LOGGER.debug("Failed to connect", exc_info=True) + raise AbortFlow("failed_to_read_config") from exc + + try: + packet_a0 = await client.read(PacketA0Notify) + except BleakError as exc: + LOGGER.debug("Failed to read data", exc_info=True) + raise AbortFlow("failed_to_read_config") from exc + finally: + await client.disconnect() + + return { + CONF_MODEL: info.name, + CONF_ADDRESS: info.address, + CONF_PROBE_COUNT: packet_a0.probe_count, + } + + +class ToGrillBluetoothConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for ToGrillBluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovery_infos: dict[str, BluetoothServiceInfoBleak] = {} + + async def _async_create_entry_internal( + self, info: BluetoothServiceInfoBleak + ) -> ConfigFlowResult: + config_data = await read_config_data(self.hass, info) + + return self.async_create_entry( + title=config_data[CONF_MODEL], + data=config_data, + ) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> ConfigFlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + if discovery_info.name not in SUPPORTED_DEVICES: + return self.async_abort(reason="not_supported") + + self._discovery_info = discovery_info + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm discovery.""" + assert self._discovery_info is not None + discovery_info = self._discovery_info + + if user_input is not None: + return await self._async_create_entry_internal(discovery_info) + + self._set_confirm_only() + placeholders = {"name": discovery_info.name} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + + return await self._async_create_entry_internal( + self._discovery_infos[address] + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, True): + address = discovery_info.address + if ( + address in current_addresses + or address in self._discovery_infos + or discovery_info.name not in SUPPORTED_DEVICES + ): + continue + self._discovery_infos[address] = discovery_info + + if not self._discovery_infos: + return self.async_abort(reason="no_devices_found") + + addresses = {info.address: info.name for info in self._discovery_infos.values()} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(addresses)}), + ) diff --git a/homeassistant/components/togrill/const.py b/homeassistant/components/togrill/const.py new file mode 100644 index 00000000000..dd2fe820919 --- /dev/null +++ b/homeassistant/components/togrill/const.py @@ -0,0 +1,8 @@ +"""Constants for the ToGrill integration.""" + +DOMAIN = "togrill" + +MAX_PROBE_COUNT = 6 + +CONF_PROBE_COUNT = "probe_count" +CONF_VERSION = "version" diff --git a/homeassistant/components/togrill/coordinator.py b/homeassistant/components/togrill/coordinator.py new file mode 100644 index 00000000000..75964067de7 --- /dev/null +++ b/homeassistant/components/togrill/coordinator.py @@ -0,0 +1,196 @@ +"""Coordinator for the ToGrill Bluetooth integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +import logging +from typing import TypeVar + +from bleak.exc import BleakError +from togrill_bluetooth.client import Client +from togrill_bluetooth.exceptions import DecodeError +from togrill_bluetooth.packets import ( + Packet, + PacketA0Notify, + PacketA1Notify, + PacketA8Write, +) + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_register_callback, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_MODEL +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_PROBE_COUNT + +type ToGrillConfigEntry = ConfigEntry[ToGrillCoordinator] + +SCAN_INTERVAL = timedelta(seconds=30) +LOGGER = logging.getLogger(__name__) + +PacketType = TypeVar("PacketType", bound=Packet) + + +def get_version_string(packet: PacketA0Notify) -> str: + """Construct a version string from packet data.""" + return f"{packet.version_major}.{packet.version_minor}" + + +class DeviceNotFound(UpdateFailed): + """Update failed due to device disconnected.""" + + +class DeviceFailed(UpdateFailed): + """Update failed due to device disconnected.""" + + +class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Packet]]): + """Class to manage fetching data.""" + + config_entry: ToGrillConfigEntry + client: Client | None = None + + def __init__( + self, + hass: HomeAssistant, + config_entry: ToGrillConfigEntry, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass=hass, + logger=LOGGER, + config_entry=config_entry, + name="ToGrill", + update_interval=SCAN_INTERVAL, + ) + self.address = config_entry.data[CONF_ADDRESS] + self.data = {} + self.device_info = DeviceInfo( + connections={(CONNECTION_BLUETOOTH, self.address)} + ) + self._packet_listeners: list[Callable[[Packet], None]] = [] + + config_entry.async_on_unload( + async_register_callback( + hass, + self._async_handle_bluetooth_event, + BluetoothCallbackMatcher(address=self.address, connectable=True), + BluetoothScanningMode.ACTIVE, + ) + ) + + @callback + def async_add_packet_listener( + self, packet_callback: Callable[[Packet], None] + ) -> Callable[[], None]: + """Add a listener for a given packet type.""" + + def _unregister(): + self._packet_listeners.remove(packet_callback) + + self._packet_listeners.append(packet_callback) + return _unregister + + def async_update_packet_listeners(self, packet: Packet): + """Update all packet listeners.""" + for listener in self._packet_listeners: + listener(packet) + + async def _connect_and_update_registry(self) -> Client: + """Update device registry data.""" + device = bluetooth.async_ble_device_from_address( + self.hass, self.address, connectable=True + ) + if not device: + raise DeviceNotFound("Unable to find device") + + try: + client = await Client.connect(device, self._notify_callback) + except BleakError as exc: + self.logger.debug("Connection failed", exc_info=True) + raise DeviceNotFound("Unable to connect to device") from exc + + try: + packet_a0 = await client.read(PacketA0Notify) + except (BleakError, DecodeError) as exc: + await client.disconnect() + raise DeviceFailed(f"Device failed {exc}") from exc + + config_entry = self.config_entry + + device_registry = dr.async_get(self.hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(CONNECTION_BLUETOOTH, self.address)}, + name=config_entry.data[CONF_MODEL], + model_id=config_entry.data[CONF_MODEL], + sw_version=get_version_string(packet_a0), + ) + + return client + + async def async_shutdown(self) -> None: + """Shutdown coordinator and disconnect from device.""" + await super().async_shutdown() + if self.client: + await self.client.disconnect() + self.client = None + + async def _get_connected_client(self) -> Client: + if self.client and not self.client.is_connected: + await self.client.disconnect() + self.client = None + if self.client: + return self.client + + self.client = await self._connect_and_update_registry() + return self.client + + def get_packet( + self, packet_type: type[PacketType], probe=None + ) -> PacketType | None: + """Get a cached packet of a certain type.""" + + if packet := self.data.get((packet_type.type, probe)): + assert isinstance(packet, packet_type) + return packet + return None + + def _notify_callback(self, packet: Packet): + probe = getattr(packet, "probe", None) + self.data[(packet.type, probe)] = packet + self.async_update_packet_listeners(packet) + self.async_update_listeners() + + async def _async_update_data(self) -> dict[tuple[int, int | None], Packet]: + """Poll the device.""" + client = await self._get_connected_client() + try: + await client.request(PacketA0Notify) + await client.request(PacketA1Notify) + for probe in range(1, self.config_entry.data[CONF_PROBE_COUNT] + 1): + await client.write(PacketA8Write(probe=probe)) + except BleakError as exc: + raise DeviceFailed(f"Device failed {exc}") from exc + return self.data + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + if not self.client and isinstance(self.last_exception, DeviceNotFound): + self.hass.async_create_task(self.async_refresh()) diff --git a/homeassistant/components/togrill/entity.py b/homeassistant/components/togrill/entity.py new file mode 100644 index 00000000000..7d956ac2d57 --- /dev/null +++ b/homeassistant/components/togrill/entity.py @@ -0,0 +1,49 @@ +"""Provides the base entities.""" + +from __future__ import annotations + +from bleak.exc import BleakError +from togrill_bluetooth.client import Client +from togrill_bluetooth.exceptions import BaseError +from togrill_bluetooth.packets import PacketWrite + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import LOGGER, ToGrillCoordinator + + +class ToGrillEntity(CoordinatorEntity[ToGrillCoordinator]): + """Coordinator entity for Gardena Bluetooth.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: ToGrillCoordinator) -> None: + """Initialize coordinator entity.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + + def _get_client(self) -> Client: + client = self.coordinator.client + if client is None or not client.is_connected: + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="disconnected" + ) + return client + + async def _write_packet(self, packet: PacketWrite) -> None: + client = self._get_client() + try: + await client.write(packet) + except BleakError as exc: + LOGGER.debug("Failed to write", exc_info=True) + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="communication_failed" + ) from exc + except BaseError as exc: + LOGGER.debug("Failed to write", exc_info=True) + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="rejected" + ) from exc + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/togrill/event.py b/homeassistant/components/togrill/event.py new file mode 100644 index 00000000000..d7d67b464d1 --- /dev/null +++ b/homeassistant/components/togrill/event.py @@ -0,0 +1,63 @@ +"""Support for event entities.""" + +from __future__ import annotations + +from togrill_bluetooth.packets import Packet, PacketA5Notify + +from homeassistant.components.event import EventEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util import slugify + +from . import ToGrillConfigEntry +from .const import CONF_PROBE_COUNT +from .coordinator import ToGrillCoordinator +from .entity import ToGrillEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ToGrillConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up event platform.""" + async_add_entities( + ToGrillEventEntity(config_entry.runtime_data, probe_number=probe_number) + for probe_number in range(1, config_entry.data[CONF_PROBE_COUNT] + 1) + ) + + +class ToGrillEventEntity(ToGrillEntity, EventEntity): + """Representation of a Hue Event entity from a button resource.""" + + def __init__(self, coordinator: ToGrillCoordinator, probe_number: int) -> None: + """Initialize the entity.""" + super().__init__(coordinator=coordinator) + + self._attr_translation_key = "event" + self._attr_translation_placeholders = {"probe_number": f"{probe_number}"} + self._attr_unique_id = f"{coordinator.address}_{probe_number}" + self._probe_number = probe_number + + self._attr_event_types: list[str] = [ + slugify(event.name) for event in PacketA5Notify.Message + ] + + self.async_on_remove(coordinator.async_add_packet_listener(self._handle_event)) + + @callback + def _handle_event(self, packet: Packet) -> None: + if not isinstance(packet, PacketA5Notify): + return + + try: + message = PacketA5Notify.Message(packet.message) + except ValueError: + return + + if packet.probe != self._probe_number: + return + + self._trigger_event(message.name.lower()) diff --git a/homeassistant/components/togrill/manifest.json b/homeassistant/components/togrill/manifest.json new file mode 100644 index 00000000000..fb56b8e3a82 --- /dev/null +++ b/homeassistant/components/togrill/manifest.json @@ -0,0 +1,19 @@ +{ + "domain": "togrill", + "name": "ToGrill", + "bluetooth": [ + { + "manufacturer_id": 34714, + "service_uuid": "0000cee0-0000-1000-8000-00805f9b34fb", + "connectable": true + } + ], + "codeowners": ["@elupus"], + "config_flow": true, + "dependencies": ["bluetooth"], + "documentation": "https://www.home-assistant.io/integrations/togrill", + "iot_class": "local_push", + "loggers": ["togrill_bluetooth"], + "quality_scale": "bronze", + "requirements": ["togrill-bluetooth==0.8.0"] +} diff --git a/homeassistant/components/togrill/number.py b/homeassistant/components/togrill/number.py new file mode 100644 index 00000000000..a87fec8d2d3 --- /dev/null +++ b/homeassistant/components/togrill/number.py @@ -0,0 +1,138 @@ +"""Support for number entities.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Any + +from togrill_bluetooth.packets import ( + PacketA0Notify, + PacketA6Write, + PacketA8Notify, + PacketA301Write, + PacketWrite, +) + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.const import UnitOfTemperature, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ToGrillConfigEntry +from .const import CONF_PROBE_COUNT, MAX_PROBE_COUNT +from .coordinator import ToGrillCoordinator +from .entity import ToGrillEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(kw_only=True, frozen=True) +class ToGrillNumberEntityDescription(NumberEntityDescription): + """Description of entity.""" + + get_value: Callable[[ToGrillCoordinator], float | None] + set_packet: Callable[[float], PacketWrite] + entity_supported: Callable[[Mapping[str, Any]], bool] = lambda _: True + + +def _get_temperature_target_description( + probe_number: int, +) -> ToGrillNumberEntityDescription: + def _set_packet(value: float | None) -> PacketWrite: + if value == 0.0: + value = None + return PacketA301Write(probe=probe_number, target=value) + + def _get_value(coordinator: ToGrillCoordinator) -> float | None: + if packet := coordinator.get_packet(PacketA8Notify, probe_number): + if packet.alarm_type == PacketA8Notify.AlarmType.TEMPERATURE_TARGET: + return packet.temperature_1 + return None + + return ToGrillNumberEntityDescription( + key=f"temperature_target_{probe_number}", + translation_key="temperature_target", + translation_placeholders={"probe_number": f"{probe_number}"}, + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + native_min_value=0, + native_max_value=250, + mode=NumberMode.BOX, + set_packet=_set_packet, + get_value=_get_value, + entity_supported=lambda x: probe_number <= x[CONF_PROBE_COUNT], + ) + + +ENTITY_DESCRIPTIONS = ( + *[ + _get_temperature_target_description(probe_number) + for probe_number in range(1, MAX_PROBE_COUNT + 1) + ], + ToGrillNumberEntityDescription( + key="alarm_interval", + translation_key="alarm_interval", + device_class=NumberDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MINUTES, + native_min_value=0, + native_max_value=15, + native_step=5, + mode=NumberMode.BOX, + set_packet=lambda x: ( + PacketA6Write(temperature_unit=None, alarm_interval=round(x)) + ), + get_value=lambda x: ( + packet.alarm_interval if (packet := x.get_packet(PacketA0Notify)) else None + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ToGrillConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up number based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + ToGrillNumber(coordinator, entity_description) + for entity_description in ENTITY_DESCRIPTIONS + if entity_description.entity_supported(entry.data) + ) + + +class ToGrillNumber(ToGrillEntity, NumberEntity): + """Representation of a number.""" + + entity_description: ToGrillNumberEntityDescription + + def __init__( + self, + coordinator: ToGrillCoordinator, + entity_description: ToGrillNumberEntityDescription, + ) -> None: + """Initialize.""" + + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = f"{coordinator.address}_{entity_description.key}" + + @property + def native_value(self) -> float | None: + """Return the value reported by the number.""" + return self.entity_description.get_value(self.coordinator) + + async def async_set_native_value(self, value: float) -> None: + """Set value on device.""" + + packet = self.entity_description.set_packet(value) + await self._write_packet(packet) diff --git a/homeassistant/components/togrill/quality_scale.yaml b/homeassistant/components/togrill/quality_scale.yaml new file mode 100644 index 00000000000..6dd44090f80 --- /dev/null +++ b/homeassistant/components/togrill/quality_scale.yaml @@ -0,0 +1,68 @@ +rules: + # Bronze + action-setup: done + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: done + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: done + config-entry-unloading: done + docs-configuration-parameters: todo + docs-installation-parameters: todo + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: + status: exempt + comment: This integration does not require authentication. + test-coverage: done + + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: done + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: done + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: + status: exempt + comment: This integration only has a single device. + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: todo + icon-translations: todo + reconfiguration-flow: todo + repair-issues: todo + stale-devices: + status: exempt + comment: This integration only has a single device. + + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: This integration does not need any websession + strict-typing: todo diff --git a/homeassistant/components/togrill/sensor.py b/homeassistant/components/togrill/sensor.py new file mode 100644 index 00000000000..1641236bfc1 --- /dev/null +++ b/homeassistant/components/togrill/sensor.py @@ -0,0 +1,129 @@ +"""Support for sensor entities.""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Any, cast + +from togrill_bluetooth.packets import Packet, PacketA0Notify, PacketA1Notify + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, + StateType, +) +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ToGrillConfigEntry +from .const import CONF_PROBE_COUNT, MAX_PROBE_COUNT +from .coordinator import ToGrillCoordinator +from .entity import ToGrillEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(kw_only=True, frozen=True) +class ToGrillSensorEntityDescription(SensorEntityDescription): + """Description of entity.""" + + packet_type: int + packet_extract: Callable[[Packet], StateType] + entity_supported: Callable[[Mapping[str, Any]], bool] = lambda _: True + + +def _get_temperature_description(probe_number: int): + def _get(packet: Packet) -> StateType: + assert isinstance(packet, PacketA1Notify) + if len(packet.temperatures) < probe_number: + return None + temperature = packet.temperatures[probe_number - 1] + if temperature is None: + return None + return temperature + + def _supported(config: Mapping[str, Any]): + return probe_number <= config[CONF_PROBE_COUNT] + + return ToGrillSensorEntityDescription( + key=f"temperature_{probe_number}", + translation_key="temperature", + translation_placeholders={"probe_number": f"{probe_number}"}, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, + packet_type=PacketA1Notify.type, + packet_extract=_get, + entity_supported=_supported, + ) + + +ENTITY_DESCRIPTIONS = ( + ToGrillSensorEntityDescription( + key="battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + packet_type=PacketA0Notify.type, + packet_extract=lambda packet: cast(PacketA0Notify, packet).battery, + ), + *[ + _get_temperature_description(probe_number) + for probe_number in range(1, MAX_PROBE_COUNT + 1) + ], +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ToGrillConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up sensor based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + ToGrillSensor(coordinator, entity_description) + for entity_description in ENTITY_DESCRIPTIONS + if entity_description.entity_supported(entry.data) + ) + + +class ToGrillSensor(ToGrillEntity, SensorEntity): + """Representation of a sensor.""" + + entity_description: ToGrillSensorEntityDescription + + def __init__( + self, + coordinator: ToGrillCoordinator, + entity_description: ToGrillSensorEntityDescription, + ) -> None: + """Initialize sensor.""" + + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.address}_{entity_description.key}" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.native_value is not None + + @property + def native_value(self) -> StateType: + """Get current value.""" + if packet := self.coordinator.data.get( + (self.entity_description.packet_type, None) + ): + return self.entity_description.packet_extract(packet) + return None diff --git a/homeassistant/components/togrill/strings.json b/homeassistant/components/togrill/strings.json new file mode 100644 index 00000000000..cef758b7d2e --- /dev/null +++ b/homeassistant/components/togrill/strings.json @@ -0,0 +1,74 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:common::config_flow::data::device%]" + }, + "data_description": { + "address": "Select the device to add." + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "failed_to_read_config": "Failed to read config from device" + } + }, + "exceptions": { + "disconnected": { + "message": "The device is disconnected" + }, + "communication_failed": { + "message": "Communication failed with the device" + }, + "rejected": { + "message": "Data was rejected by device" + } + }, + "entity": { + "sensor": { + "temperature": { + "name": "Probe {probe_number}" + } + }, + "number": { + "temperature_target": { + "name": "Target {probe_number}" + }, + "alarm_interval": { + "name": "Alarm interval" + } + }, + "event": { + "event": { + "name": "Probe {probe_number}", + "state_attributes": { + "event_type": { + "state": { + "probe_acknowledge": "Alarm acknowledged", + "probe_alarm": "Alarm triggered", + "probe_disconnected": "Probe disconnected", + "device_low_power": "Device has low battery", + "device_high_temp": "Device has too high temperature", + "probe_below_minimum": "Temperature too low", + "probe_above_maximum": "Temperature too high", + "ignition_failure": "Ignition failure", + "ambient_low_temp": "Ambient temperature too low", + "ambient_over_heat": "Ambient temperature too high", + "ambient_cool_down": "Ambient temperature cooldown", + "probe_timer_alarm": "Timer alarm" + } + } + } + } + } + } +} diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 08e1991d831..f288f011061 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -197,7 +197,7 @@ SENSOR_TYPES = ( attribute=TMRW_ATTR_PRECIPITATION_TYPE, value_map=PrecipitationType, ), - # Data comes in as ppb, convert to µg/m^3 + # Data comes in as ppb, convert to μg/m^3 # Molecular weight of Ozone is 48 TomorrowioSensorEntityDescription( key="ozone", @@ -221,7 +221,7 @@ SENSOR_TYPES = ( device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), - # Data comes in as ppb, convert to µg/m^3 + # Data comes in as ppb, convert to μg/m^3 # Molecular weight of Nitrogen Dioxide is 46.01 TomorrowioSensorEntityDescription( key="nitrogen_dioxide", @@ -240,7 +240,7 @@ SENSOR_TYPES = ( device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), - # Data comes in as ppb, convert to µg/m^3 + # Data comes in as ppb, convert to μg/m^3 # Molecular weight of Sulphur Dioxide is 64.07 TomorrowioSensorEntityDescription( key="sulphur_dioxide", diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 2f3802dc9a6..7cc8d7a5ebc 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -172,9 +172,9 @@ class TotalConnectZoneBinarySensor(TotalConnectZoneEntity, BinarySensorEntity): super().__init__(coordinator, zone, location_id, entity_description.key) self.entity_description = entity_description self._attr_extra_state_attributes = { - "zone_id": zone.zoneid, + "zone_id": str(zone.zoneid), "location_id": location_id, - "partition": zone.partition, + "partition": str(zone.partition), } @property diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 3f5d05fda13..33e82dcaf53 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -105,11 +105,7 @@ class TotalConnectConfigFlow(ConfigFlow, domain=DOMAIN): }, ) else: - # Force the loading of locations using I/O - number_locations = await self.hass.async_add_executor_job( - self.client.get_number_locations, - ) - if number_locations < 1: + if self.client.get_number_locations() < 1: return self.async_abort(reason="no_locations") for location_id in self.client.locations: self.usercodes[location_id] = None diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 6aff1ea392b..cd349cd3414 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/totalconnect", "iot_class": "cloud_polling", "loggers": ["total_connect_client"], - "requirements": ["total-connect-client==2025.1.4"] + "requirements": ["total-connect-client==2025.5"] } diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index cf9099448df..629332d9d64 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -976,11 +976,15 @@ class SpeechManager: if engine_instance.name is None or engine_instance.name is UNDEFINED: raise HomeAssistantError("TTS engine name is not set.") - if isinstance(engine_instance, Provider): + if isinstance(engine_instance, Provider) or ( + not engine_instance.async_supports_streaming_input() + ): + # Non-streaming if isinstance(message_or_stream, str): message = message_or_stream else: message = "".join([chunk async for chunk in message_or_stream]) + extension, data = await engine_instance.async_internal_get_tts_audio( message, language, options ) @@ -996,6 +1000,7 @@ class SpeechManager: data_gen = make_data_generator(data) else: + # Streaming if isinstance(message_or_stream, str): async def gen_stream() -> AsyncGenerator[str]: diff --git a/homeassistant/components/tts/entity.py b/homeassistant/components/tts/entity.py index aea5be6d0da..77abaa26bab 100644 --- a/homeassistant/components/tts/entity.py +++ b/homeassistant/components/tts/entity.py @@ -191,6 +191,18 @@ class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH """Load tts audio file from the engine.""" raise NotImplementedError + @final + async def async_internal_get_tts_audio( + self, message: str, language: str, options: dict[str, Any] + ) -> TtsAudioType: + """Load tts audio file from the engine and update state. + + Return a tuple of file extension and data as bytes. + """ + self.__last_tts_loaded = dt_util.utcnow().isoformat() + self.async_write_ha_state() + return await self.async_get_tts_audio(message, language, options=options) + async def async_get_tts_audio( self, message: str, language: str, options: dict[str, Any] ) -> TtsAudioType: diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index fd3f0cfcb7e..f9bc973f5a1 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -314,8 +314,8 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), - # Zigbee gateway - # Undocumented + # Gateway control + # https://developer.tuya.com/en/docs/iot/wg?id=Kbcdadk79ejok "wg2": ( TuyaBinarySensorEntityDescription( key=DPCode.MASTER_STATE, @@ -324,6 +324,15 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { on_value="alarm", ), ), + # Thermostat + # https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9 + "wk": ( + TuyaBinarySensorEntityDescription( + key=DPCode.VALVE_STATE, + translation_key="valve", + on_value="open", + ), + ), # Thermostatic Radiator Valve # Not documented "wkf": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 38661d548a7..7a80a51726d 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -24,6 +24,7 @@ from homeassistant.const import ( UnitOfPressure, UnitOfTemperature, UnitOfVolume, + UnitOfVolumetricFlux, ) DOMAIN = "tuya" @@ -66,6 +67,7 @@ PLATFORMS = [ Platform.SIREN, Platform.SWITCH, Platform.VACUUM, + Platform.VALVE, ] @@ -99,11 +101,11 @@ class DPCode(StrEnum): AIR_QUALITY = "air_quality" AIR_QUALITY_INDEX = "air_quality_index" ALARM_DELAY_TIME = "alarm_delay_time" + ALARM_MESSAGE = "alarm_message" + ALARM_MSG = "alarm_msg" ALARM_SWITCH = "alarm_switch" # Alarm switch ALARM_TIME = "alarm_time" # Alarm time ALARM_VOLUME = "alarm_volume" # Alarm volume - ALARM_MESSAGE = "alarm_message" - ALARM_MSG = "alarm_msg" ANGLE_HORIZONTAL = "angle_horizontal" ANGLE_VERTICAL = "angle_vertical" ANION = "anion" # Ionizer unit @@ -145,8 +147,8 @@ class DPCode(StrEnum): CLEAN_AREA = "clean_area" CLEAN_TIME = "clean_time" CLICK_SUSTAIN_TIME = "click_sustain_time" - CLOUD_RECIPE_NUMBER = "cloud_recipe_number" CLOSED_OPENED_KIT = "closed_opened_kit" + CLOUD_RECIPE_NUMBER = "cloud_recipe_number" CO_STATE = "co_state" CO_STATUS = "co_status" CO_VALUE = "co_value" @@ -157,15 +159,23 @@ class DPCode(StrEnum): COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode - COOK_TEMPERATURE = "cook_temperature" - COOK_TIME = "cook_time" CONCENTRATION_SET = "concentration_set" # Concentration setting CONTROL = "control" CONTROL_2 = "control_2" CONTROL_3 = "control_3" CONTROL_BACK = "control_back" CONTROL_BACK_MODE = "control_back_mode" + COOK_TEMPERATURE = "cook_temperature" + COOK_TIME = "cook_time" COUNTDOWN = "countdown" # Countdown + COUNTDOWN_1 = "countdown_1" + COUNTDOWN_2 = "countdown_2" + COUNTDOWN_3 = "countdown_3" + COUNTDOWN_4 = "countdown_4" + COUNTDOWN_5 = "countdown_5" + COUNTDOWN_6 = "countdown_6" + COUNTDOWN_7 = "countdown_7" + COUNTDOWN_8 = "countdown_8" COUNTDOWN_LEFT = "countdown_left" COUNTDOWN_SET = "countdown_set" # Countdown setting CRY_DETECTION_SWITCH = "cry_detection_switch" @@ -192,11 +202,11 @@ class DPCode(StrEnum): FAN_COOL = "fan_cool" # Cool wind FAN_DIRECTION = "fan_direction" # Fan direction FAN_HORIZONTAL = "fan_horizontal" # Horizontal swing flap angle + FAN_MODE = "fan_mode" FAN_SPEED = "fan_speed" FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed FAN_SWITCH = "fan_switch" - FAN_MODE = "fan_mode" FAN_VERTICAL = "fan_vertical" # Vertical swing flap angle FAR_DETECTION = "far_detection" FAULT = "fault" @@ -209,6 +219,7 @@ class DPCode(StrEnum): FLOODLIGHT_LIGHTNESS = "floodlight_lightness" FLOODLIGHT_SWITCH = "floodlight_switch" FORWARD_ENERGY_TOTAL = "forward_energy_total" + FROST = "frost" # Frost protection GAS_SENSOR_STATE = "gas_sensor_state" GAS_SENSOR_STATUS = "gas_sensor_status" GAS_SENSOR_VALUE = "gas_sensor_value" @@ -238,10 +249,10 @@ class DPCode(StrEnum): LIQUID_LEVEL_PERCENT = "liquid_level_percent" LIQUID_STATE = "liquid_state" LOCK = "lock" # Lock / Child lock - MASTER_MODE = "master_mode" # alarm mode - MASTER_STATE = "master_state" # alarm state MACH_OPERATE = "mach_operate" MANUAL_FEED = "manual_feed" + MASTER_MODE = "master_mode" # alarm mode + MASTER_STATE = "master_state" # alarm state MATERIAL = "material" # Material MAX_SET = "max_set" MINI_SET = "mini_set" @@ -255,6 +266,7 @@ class DPCode(StrEnum): MUFFLING = "muffling" # Muffling NEAR_DETECTION = "near_detection" OPPOSITE = "opposite" + OXYGEN = "oxygen" # Oxygen bar PAUSE = "pause" PERCENT_CONTROL = "percent_control" PERCENT_CONTROL_2 = "percent_control_2" @@ -262,7 +274,6 @@ class DPCode(StrEnum): PERCENT_STATE = "percent_state" PERCENT_STATE_2 = "percent_state_2" PERCENT_STATE_3 = "percent_state_3" - POSITION = "position" PHASE_A = "phase_a" PHASE_B = "phase_b" PHASE_C = "phase_c" @@ -272,20 +283,22 @@ class DPCode(StrEnum): PM25 = "pm25" PM25_STATE = "pm25_state" PM25_VALUE = "pm25_value" + POSITION = "position" POWDER_SET = "powder_set" # Powder POWER = "power" POWER_GO = "power_go" + POWER_TOTAL = "power_total" PREHEAT = "preheat" PREHEAT_1 = "preheat_1" PREHEAT_2 = "preheat_2" - POWER_TOTAL = "power_total" PRESENCE_STATE = "presence_state" PRESSURE_STATE = "pressure_state" PRESSURE_VALUE = "pressure_value" PUMP = "pump" PUMP_RESET = "pump_reset" # Water pump reset PUMP_TIME = "pump_time" # Water pump duration - OXYGEN = "oxygen" # Oxygen bar + RAIN_24H = "rain_24h" # Total daily rainfall in mm + RAIN_RATE = "rain_rate" # Rain intensity in mm/h RECORD_MODE = "record_mode" RECORD_SWITCH = "record_switch" # Recording switch RELAY_STATUS = "relay_status" @@ -318,6 +331,7 @@ class DPCode(StrEnum): STATUS = "status" STERILIZATION = "sterilization" # Sterilization SUCTION = "suction" + SUPPLY_FREQUENCY = "supply_frequency" SWING = "swing" # Swing mode SWITCH = "switch" # Switch SWITCH_1 = "switch_1" # Switch 1 @@ -368,7 +382,6 @@ class DPCode(StrEnum): TEMP_CONTROLLER = "temp_controller" TEMP_CORRECTION = "temp_correction" TEMP_CURRENT = "temp_current" # Current temperature in °C - TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F TEMP_CURRENT_EXTERNAL = ( "temp_current_external" # Current external temperature in Celsius ) @@ -384,6 +397,7 @@ class DPCode(StrEnum): TEMP_CURRENT_EXTERNAL_F = ( "temp_current_external_f" # Current external temperature in Fahrenheit ) + TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F TEMP_INDOOR = "temp_indoor" # Indoor temperature in °C TEMP_SET = "temp_set" # Set the temperature in °C TEMP_SET_F = "temp_set_f" # Set the temperature in °F @@ -397,17 +411,19 @@ class DPCode(StrEnum): TOTAL_CLEAN_COUNT = "total_clean_count" TOTAL_CLEAN_TIME = "total_clean_time" TOTAL_FORWARD_ENERGY = "total_forward_energy" - TOTAL_TIME = "total_time" TOTAL_PM = "total_pm" TOTAL_POWER = "total_power" + TOTAL_TIME = "total_time" TVOC = "tvoc" UPPER_TEMP = "upper_temp" UPPER_TEMP_F = "upper_temp_f" UV = "uv" # UV sterilization + UV_INDEX = "uv_index" UV_RUNTIME = "uv_runtime" # UV runtime VA_BATTERY = "va_battery" VA_HUMIDITY = "va_humidity" VA_TEMPERATURE = "va_temperature" + VALVE_STATE = "valve_state" VOC_STATE = "voc_state" VOC_VALUE = "voc_value" VOICE_SWITCH = "voice_switch" @@ -416,10 +432,10 @@ class DPCode(StrEnum): WARM = "warm" # Heat preservation WARM_TIME = "warm_time" # Heat preservation time WATER = "water" + WATER_LEVEL = "water_level" WATER_RESET = "water_reset" # Resetting of water usage days WATER_SET = "water_set" # Water level WATER_TIME = "water_time" # Water usage duration - WATER_LEVEL = "water_level" WATERSENSOR_STATE = "watersensor_state" WEATHER_DELAY = "weather_delay" WET = "wet" # Humidification @@ -427,6 +443,7 @@ class DPCode(StrEnum): WINDOW_STATE = "window_state" WINDSPEED = "windspeed" WINDSPEED_AVG = "windspeed_avg" + WIND_DIRECT = "wind_direct" WIRELESS_BATTERYLOCK = "wireless_batterylock" WIRELESS_ELECTRICITY = "wireless_electricity" WORK_MODE = "work_mode" # Working mode @@ -512,6 +529,11 @@ UNITS = ( aliases={"m3"}, device_classes={SensorDeviceClass.GAS}, ), + UnitOfMeasurement( + unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + aliases={"mm"}, + device_classes={SensorDeviceClass.PRECIPITATION_INTENSITY}, + ), UnitOfMeasurement( unit=LIGHT_LUX, aliases={"lux"}, @@ -519,7 +541,9 @@ UNITS = ( ), UnitOfMeasurement( unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - aliases={"ug/m3", "µg/m3", "ug/m³"}, + # The μ-char has 2 unicode variants \u00b5 and \u03bc + # The \u03bc variant (Greek Mu char) is recommended + aliases={"ug/m3", "\u03bcg/m3", "\u00b5g/m3", "ug/m³"}, device_classes={ SensorDeviceClass.NITROGEN_DIOXIDE, SensorDeviceClass.NITROGEN_MONOXIDE, diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index fba42ad76cf..12b6b11a297 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -26,6 +26,16 @@ from .entity import TuyaEntity from .models import EnumTypeData, IntegerTypeData from .util import get_dpcode +_DIRECTION_DPCODES = (DPCode.FAN_DIRECTION,) +_OSCILLATE_DPCODES = (DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL) +_SPEED_DPCODES = ( + DPCode.FAN_SPEED_PERCENT, + DPCode.FAN_SPEED, + DPCode.SPEED, + DPCode.FAN_SPEED_ENUM, +) +_SWITCH_DPCODES = (DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH) + TUYA_SUPPORT_TYPE = { # Dehumidifier # https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha @@ -47,6 +57,19 @@ TUYA_SUPPORT_TYPE = { } +def _has_a_valid_dpcode(device: CustomerDevice) -> bool: + """Check if the device has at least one valid DP code.""" + properties_to_check: list[DPCode | tuple[DPCode, ...] | None] = [ + # Main control switch + _SWITCH_DPCODES, + # Other properties + _SPEED_DPCODES, + _OSCILLATE_DPCODES, + _DIRECTION_DPCODES, + ] + return any(get_dpcode(device, code) for code in properties_to_check) + + async def async_setup_entry( hass: HomeAssistant, entry: TuyaConfigEntry, @@ -61,7 +84,7 @@ async def async_setup_entry( entities: list[TuyaFanEntity] = [] for device_id in device_ids: device = hass_data.manager.device_map[device_id] - if device and device.category in TUYA_SUPPORT_TYPE: + if device.category in TUYA_SUPPORT_TYPE and _has_a_valid_dpcode(device): entities.append(TuyaFanEntity(device, hass_data.manager)) async_add_entities(entities) @@ -91,9 +114,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): """Init Tuya Fan Device.""" super().__init__(device, device_manager) - self._switch = get_dpcode( - self.device, (DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH) - ) + self._switch = get_dpcode(self.device, _SWITCH_DPCODES) self._attr_preset_modes = [] if enum_type := self.find_dpcode( @@ -104,31 +125,23 @@ class TuyaFanEntity(TuyaEntity, FanEntity): self._attr_preset_modes = enum_type.range # Find speed controls, can be either percentage or a set of speeds - dpcodes = ( - DPCode.FAN_SPEED_PERCENT, - DPCode.FAN_SPEED, - DPCode.SPEED, - DPCode.FAN_SPEED_ENUM, - ) if int_type := self.find_dpcode( - dpcodes, dptype=DPType.INTEGER, prefer_function=True + _SPEED_DPCODES, dptype=DPType.INTEGER, prefer_function=True ): self._attr_supported_features |= FanEntityFeature.SET_SPEED self._speed = int_type elif enum_type := self.find_dpcode( - dpcodes, dptype=DPType.ENUM, prefer_function=True + _SPEED_DPCODES, dptype=DPType.ENUM, prefer_function=True ): self._attr_supported_features |= FanEntityFeature.SET_SPEED self._speeds = enum_type - if dpcode := get_dpcode( - self.device, (DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL) - ): + if dpcode := get_dpcode(self.device, _OSCILLATE_DPCODES): self._oscillate = dpcode self._attr_supported_features |= FanEntityFeature.OSCILLATE if enum_type := self.find_dpcode( - DPCode.FAN_DIRECTION, dptype=DPType.ENUM, prefer_function=True + _DIRECTION_DPCODES, dptype=DPType.ENUM, prefer_function=True ): self._direction = enum_type self._attr_supported_features |= FanEntityFeature.DIRECTION diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 5def5c5e16c..cb08ccaf476 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -35,6 +35,20 @@ class TuyaHumidifierEntityDescription(HumidifierEntityDescription): humidity: DPCode | None = None +def _has_a_valid_dpcode( + device: CustomerDevice, description: TuyaHumidifierEntityDescription +) -> bool: + """Check if the device has at least one valid DP code.""" + properties_to_check: list[DPCode | tuple[DPCode, ...] | None] = [ + # Main control switch + description.dpcode or DPCode(description.key), + # Other humidity properties + description.current_humidity, + description.humidity, + ] + return any(get_dpcode(device, code) for code in properties_to_check) + + HUMIDIFIERS: dict[str, TuyaHumidifierEntityDescription] = { # Dehumidifier # https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha @@ -71,7 +85,9 @@ async def async_setup_entry( entities: list[TuyaHumidifierEntity] = [] for device_id in device_ids: device = hass_data.manager.device_map[device_id] - if description := HUMIDIFIERS.get(device.category): + if ( + description := HUMIDIFIERS.get(device.category) + ) and _has_a_valid_dpcode(device, description): entities.append( TuyaHumidifierEntity(device, hass_data.manager, description) ) diff --git a/homeassistant/components/tuya/icons.json b/homeassistant/components/tuya/icons.json index 40bbf41fd0d..04a701b4764 100644 --- a/homeassistant/components/tuya/icons.json +++ b/homeassistant/components/tuya/icons.json @@ -15,6 +15,9 @@ }, "tilt": { "default": "mdi:spirit-level" + }, + "valve": { + "default": "mdi:pipe-valve" } }, "button": { diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 9848351047c..673e9b1ffb3 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -663,8 +663,11 @@ class TuyaLightEntity(TuyaEntity, LightEntity): }, ] - elif ATTR_BRIGHTNESS in kwargs and self._brightness: - brightness = kwargs[ATTR_BRIGHTNESS] + elif self._brightness and (ATTR_BRIGHTNESS in kwargs or ATTR_WHITE in kwargs): + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + else: + brightness = kwargs[ATTR_WHITE] # If there is a min/max value, the brightness is actually limited. # Meaning it is actually not on a 0-255 scale. diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index 43e4c04c518..82cf5ebd200 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -99,23 +99,23 @@ class EnumTypeData: return cls(dpcode, **parsed) -class ComplexTypeData: - """Complex Type Data (for JSON/RAW parsing).""" +class ComplexValue: + """Complex value (for JSON/RAW parsing).""" @classmethod def from_json(cls, data: str) -> Self: - """Load JSON string and return a ComplexTypeData object.""" + """Load JSON string and return a ComplexValue object.""" raise NotImplementedError("from_json is not implemented for this type") @classmethod - def from_raw(cls, data: str) -> Self: - """Decode base64 string and return a ComplexTypeData object.""" + def from_raw(cls, data: str) -> Self | None: + """Decode base64 string and return a ComplexValue object.""" raise NotImplementedError("from_raw is not implemented for this type") @dataclass -class ElectricityTypeData(ComplexTypeData): - """Electricity Type Data.""" +class ElectricityValue(ComplexValue): + """Electricity complex value.""" electriccurrent: str | None = None power: str | None = None @@ -123,13 +123,15 @@ class ElectricityTypeData(ComplexTypeData): @classmethod def from_json(cls, data: str) -> Self: - """Load JSON string and return a ElectricityTypeData object.""" + """Load JSON string and return a ElectricityValue object.""" return cls(**json.loads(data.lower())) @classmethod - def from_raw(cls, data: str) -> Self: - """Decode base64 string and return a ElectricityTypeData object.""" + def from_raw(cls, data: str) -> Self | None: + """Decode base64 string and return a ElectricityValue object.""" raw = base64.b64decode(data) + if len(raw) == 0: + return None voltage = struct.unpack(">H", raw[0:2])[0] / 10.0 electriccurrent = struct.unpack(">L", b"\x00" + raw[2:5])[0] / 1000.0 power = struct.unpack(">L", b"\x00" + raw[5:8])[0] / 1000.0 diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 88216ae3d06..7fadaa0489b 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -224,6 +224,66 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Smart Water Timer + "sfkzq": ( + # Controls the irrigation duration for the water valve + NumberEntityDescription( + key=DPCode.COUNTDOWN_1, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "1"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COUNTDOWN_2, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "2"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COUNTDOWN_3, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "3"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COUNTDOWN_4, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "4"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COUNTDOWN_5, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "5"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COUNTDOWN_6, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "6"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COUNTDOWN_7, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "7"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COUNTDOWN_8, + translation_key="indexed_irrigation_duration", + translation_placeholders={"index": "8"}, + device_class=NumberDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 9eb05186f63..fe7db2b28b9 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -2,7 +2,9 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from typing import Any from tuya_sharing import CustomerDevice, Manager from tuya_sharing.device import DeviceStatusRange @@ -40,15 +42,35 @@ from .const import ( UnitOfMeasurement, ) from .entity import TuyaEntity -from .models import ComplexTypeData, ElectricityTypeData, EnumTypeData, IntegerTypeData +from .models import ComplexValue, ElectricityValue, EnumTypeData, IntegerTypeData + +_WIND_DIRECTIONS = { + "north": 0.0, + "north_north_east": 22.5, + "north_east": 45.0, + "east_north_east": 67.5, + "east": 90.0, + "east_south_east": 112.5, + "south_east": 135.0, + "south_south_east": 157.5, + "south": 180.0, + "south_south_west": 202.5, + "south_west": 225.0, + "west_south_west": 247.5, + "west": 270.0, + "west_north_west": 292.5, + "north_west": 315.0, + "north_north_west": 337.5, +} @dataclass(frozen=True) class TuyaSensorEntityDescription(SensorEntityDescription): """Describes Tuya sensor entity.""" - complex_type: type[ComplexTypeData] | None = None + complex_type: type[ComplexValue] | None = None subkey: str | None = None + state_conversion: Callable[[Any], StateType] | None = None # Commonly used battery sensors, that are reused in the sensors down below. @@ -363,13 +385,20 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + TuyaSensorEntityDescription( + key=DPCode.SUPPLY_FREQUENCY, + translation_key="supply_frequency", + device_class=SensorDeviceClass.FREQUENCY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, translation_key="phase_a_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -378,7 +407,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="power", ), TuyaSensorEntityDescription( @@ -387,7 +416,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="voltage", ), TuyaSensorEntityDescription( @@ -396,7 +425,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -405,7 +434,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="power", ), TuyaSensorEntityDescription( @@ -414,7 +443,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="voltage", ), TuyaSensorEntityDescription( @@ -423,7 +452,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -432,7 +461,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="power", ), TuyaSensorEntityDescription( @@ -441,7 +470,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="voltage", ), TuyaSensorEntityDescription( @@ -924,6 +953,30 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), + TuyaSensorEntityDescription( + key=DPCode.RAIN_24H, + translation_key="precipitation_today", + device_class=SensorDeviceClass.PRECIPITATION, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + TuyaSensorEntityDescription( + key=DPCode.RAIN_RATE, + translation_key="precipitation_intensity", + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.UV_INDEX, + translation_key="uv_index", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.WIND_DIRECT, + translation_key="wind_direction", + device_class=SensorDeviceClass.WIND_DIRECTION, + state_class=SensorStateClass.MEASUREMENT, + state_conversion=lambda state: _WIND_DIRECTIONS.get(str(state)), + ), *BATTERY_SENSORS, ), # Gas Detector @@ -1334,7 +1387,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.REVERSE_ENERGY_TOTAL, - translation_key="total_energy", + translation_key="total_production", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -1343,16 +1396,23 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { translation_key="total_power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="power", ), + TuyaSensorEntityDescription( + key=DPCode.SUPPLY_FREQUENCY, + translation_key="supply_frequency", + device_class=SensorDeviceClass.FREQUENCY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, translation_key="phase_a_current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1361,7 +1421,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="power", ), TuyaSensorEntityDescription( @@ -1370,7 +1430,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1379,7 +1439,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1388,7 +1448,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="power", ), TuyaSensorEntityDescription( @@ -1397,7 +1457,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="voltage", ), TuyaSensorEntityDescription( @@ -1406,7 +1466,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, state_class=SensorStateClass.MEASUREMENT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="electriccurrent", ), TuyaSensorEntityDescription( @@ -1415,7 +1475,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="power", ), TuyaSensorEntityDescription( @@ -1424,7 +1484,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, - complex_type=ElectricityTypeData, + complex_type=ElectricityValue, subkey="voltage", ), ), @@ -1445,6 +1505,12 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { suggested_display_precision=0, suggested_unit_of_measurement=UnitOfPower.WATT, ), + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + translation_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), ), # Pool HeatPump "znrb": ( @@ -1523,7 +1589,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): _status_range: DeviceStatusRange | None = None _type: DPType | None = None - _type_data: IntegerTypeData | EnumTypeData | ComplexTypeData | None = None + _type_data: IntegerTypeData | EnumTypeData | None = None _uom: UnitOfMeasurement | None = None def __init__( @@ -1575,6 +1641,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): self.unique_id, ) self._attr_device_class = None + self._attr_suggested_unit_of_measurement = None return uoms = DEVICE_CLASS_UNITS[self.device_class] @@ -1585,6 +1652,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): # Unknown unit of measurement, device class should not be used. if uom is None: self._attr_device_class = None + self._attr_suggested_unit_of_measurement = None return # Found unit of measurement, use the standardized Unit @@ -1609,6 +1677,10 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): if value is None: return None + # Convert value, if required + if (convert := self.entity_description.state_conversion) is not None: + return convert(value) + # Scale integer/float value if isinstance(self._type_data, IntegerTypeData): return self._type_data.scale_value(value) @@ -1634,10 +1706,11 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): if ( self.entity_description.complex_type is None or self.entity_description.subkey is None + or (raw_values := self.entity_description.complex_type.from_raw(value)) + is None ): return None - values = self.entity_description.complex_type.from_raw(value) - return getattr(values, self.entity_description.subkey) + return getattr(raw_values, self.entity_description.subkey) # Valid string or enum value return value diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index d660c9c910d..fa15e34694c 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -63,6 +63,9 @@ "defrost": { "name": "Defrost" }, + "valve": { + "name": "Valve" + }, "wet": { "name": "Wet" } @@ -145,6 +148,9 @@ "heat_preservation_time": { "name": "Heat preservation time" }, + "indexed_irrigation_duration": { + "name": "Irrigation duration {index}" + }, "feed": { "name": "Feed" }, @@ -472,7 +478,7 @@ }, "blanket_level": { "state": { - "level_1": "Low", + "level_1": "[%key:common::state::low%]", "level_2": "Level 2", "level_3": "Level 3", "level_4": "Level 4", @@ -481,7 +487,7 @@ "level_7": "Level 7", "level_8": "Level 8", "level_9": "Level 9", - "level_10": "High" + "level_10": "[%key:common::state::high%]" } }, "odor_elimination_mode": { @@ -529,6 +535,18 @@ "air_pressure": { "name": "Air pressure" }, + "precipitation_today": { + "name": "Total precipitation today" + }, + "precipitation_intensity": { + "name": "[%key:component::sensor::entity_component::precipitation_intensity::name%]" + }, + "uv_index": { + "name": "UV index" + }, + "wind_direction": { + "name": "[%key:component::sensor::entity_component::wind_direction::name%]" + }, "pm25": { "name": "[%key:component::sensor::entity_component::pm25::name%]" }, @@ -737,6 +755,9 @@ }, "liquid_level": { "name": "Liquid level" + }, + "supply_frequency": { + "name": "Supply frequency" } }, "switch": { @@ -892,6 +913,14 @@ }, "siren": { "name": "Siren" + }, + "frost_protection": { + "name": "Frost protection" + } + }, + "valve": { + "indexed_valve": { + "name": "Valve {index}" } } }, diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index ecd7d9f4f44..b9edc82ad71 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -227,6 +227,15 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Ceiling Fan Light + # https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v + "fsd": ( + SwitchEntityDescription( + key=DPCode.FAN_BEEP, + translation_key="sound", + entity_category=EntityCategory.CONFIG, + ), + ), # Irrigator # https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k "ggq": ( @@ -785,6 +794,15 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Gateway control + # https://developer.tuya.com/en/docs/iot/wg?id=Kbcdadk79ejok + "wg2": ( + SwitchEntityDescription( + key=DPCode.MUFFLING, + translation_key="mute", + entity_category=EntityCategory.CONFIG, + ), + ), # Thermostat # https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9 "wk": ( @@ -793,6 +811,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { translation_key="child_lock", entity_category=EntityCategory.CONFIG, ), + SwitchEntityDescription( + key=DPCode.FROST, + translation_key="frost_protection", + entity_category=EntityCategory.CONFIG, + ), ), # Two-way temperature and humidity switch # "MOES Temperature and Humidity Smart Switch Module MS-103" @@ -867,6 +890,15 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Smoke Detector + # https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952 + "ywbj": ( + SwitchEntityDescription( + key=DPCode.MUFFLING, + translation_key="mute", + entity_category=EntityCategory.CONFIG, + ), + ), # Smart Electricity Meter # https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7 "zndb": ( diff --git a/homeassistant/components/tuya/valve.py b/homeassistant/components/tuya/valve.py new file mode 100644 index 00000000000..06218c7030f --- /dev/null +++ b/homeassistant/components/tuya/valve.py @@ -0,0 +1,140 @@ +"""Support for Tuya valves.""" + +from __future__ import annotations + +from tuya_sharing import CustomerDevice, Manager + +from homeassistant.components.valve import ( + ValveDeviceClass, + ValveEntity, + ValveEntityDescription, + ValveEntityFeature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import TuyaConfigEntry +from .const import TUYA_DISCOVERY_NEW, DPCode +from .entity import TuyaEntity + +# All descriptions can be found here. Mostly the Boolean data types in the +# default instruction set of each category end up being a Valve. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +VALVES: dict[str, tuple[ValveEntityDescription, ...]] = { + # Smart Water Timer + "sfkzq": ( + ValveEntityDescription( + key=DPCode.SWITCH_1, + translation_key="indexed_valve", + translation_placeholders={"index": "1"}, + device_class=ValveDeviceClass.WATER, + ), + ValveEntityDescription( + key=DPCode.SWITCH_2, + translation_key="indexed_valve", + translation_placeholders={"index": "2"}, + device_class=ValveDeviceClass.WATER, + ), + ValveEntityDescription( + key=DPCode.SWITCH_3, + translation_key="indexed_valve", + translation_placeholders={"index": "3"}, + device_class=ValveDeviceClass.WATER, + ), + ValveEntityDescription( + key=DPCode.SWITCH_4, + translation_key="indexed_valve", + translation_placeholders={"index": "4"}, + device_class=ValveDeviceClass.WATER, + ), + ValveEntityDescription( + key=DPCode.SWITCH_5, + translation_key="indexed_valve", + translation_placeholders={"index": "5"}, + device_class=ValveDeviceClass.WATER, + ), + ValveEntityDescription( + key=DPCode.SWITCH_6, + translation_key="indexed_valve", + translation_placeholders={"index": "6"}, + device_class=ValveDeviceClass.WATER, + ), + ValveEntityDescription( + key=DPCode.SWITCH_7, + translation_key="indexed_valve", + translation_placeholders={"index": "7"}, + device_class=ValveDeviceClass.WATER, + ), + ValveEntityDescription( + key=DPCode.SWITCH_8, + translation_key="indexed_valve", + translation_placeholders={"index": "8"}, + device_class=ValveDeviceClass.WATER, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: TuyaConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up tuya valves dynamically through tuya discovery.""" + hass_data = entry.runtime_data + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered tuya valve.""" + entities: list[TuyaValveEntity] = [] + for device_id in device_ids: + device = hass_data.manager.device_map[device_id] + if descriptions := VALVES.get(device.category): + entities.extend( + TuyaValveEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaValveEntity(TuyaEntity, ValveEntity): + """Tuya Valve Device.""" + + _attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + + def __init__( + self, + device: CustomerDevice, + device_manager: Manager, + description: ValveEntityDescription, + ) -> None: + """Init TuyaValveEntity.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + @property + def is_closed(self) -> bool: + """Return if the valve is closed.""" + return not self.device.status.get(self.entity_description.key, False) + + async def async_open_valve(self) -> None: + """Open the valve.""" + await self.hass.async_add_executor_job( + self._send_command, [{"code": self.entity_description.key, "value": True}] + ) + + async def async_close_valve(self) -> None: + """Close the valve.""" + await self.hass.async_add_executor_job( + self._send_command, [{"code": self.entity_description.key, "value": False}] + ) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index d13b180d62d..c766af47951 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -7,7 +7,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["aiounifi"], - "requirements": ["aiounifi==84"], + "requirements": ["aiounifi==86"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 5fa9a85d341..97a5ca67186 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -8,7 +8,7 @@ import logging from aiohttp.client_exceptions import ServerDisconnectedError from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import Bootstrap -from uiprotect.exceptions import ClientError, NotAuthorized +from uiprotect.exceptions import BadRequest, ClientError, NotAuthorized # Import the test_util.anonymize module from the uiprotect package # in __init__ to ensure it gets imported in the executor since the @@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool: new_api_key = await protect.create_api_key( name=f"Home Assistant ({hass.config.location_name})" ) - except NotAuthorized as err: + except (NotAuthorized, BadRequest) as err: _LOGGER.error("Failed to create API key: %s", err) else: protect.set_api_key(new_api_key) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 8eee080abb4..50bdeec8572 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.20.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.21.1", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 708a4883ddd..5a3dcc6ddfd 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -60,43 +60,31 @@ ALL_GLOBAL_SERIVCES = [ SERVICE_GET_USER_KEYRING_INFO, ] -DOORBELL_TEXT_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - vol.Required(ATTR_MESSAGE): cv.string, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +DOORBELL_TEXT_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_MESSAGE): cv.string, + }, ) -CHIME_PAIRED_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - "doorbells": cv.TARGET_SERVICE_FIELDS, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +CHIME_PAIRED_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + "doorbells": cv.ENTITY_SERVICE_FIELDS, + }, ) -REMOVE_PRIVACY_ZONE_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - vol.Required(ATTR_NAME): cv.string, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +REMOVE_PRIVACY_ZONE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_NAME): cv.string, + }, ) -GET_USER_KEYRING_INFO_SCHEMA = vol.All( - vol.Schema( - { - **cv.ENTITY_SERVICE_FIELDS, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID), +GET_USER_KEYRING_INFO_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + }, ) diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index bb414fa95f8..750cffaf1e2 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -21,7 +21,6 @@ "step": { "init": { "data": { - "scan_interval": "Update interval (seconds, minimal 30)", "force_poll": "Force polling of all data" } } diff --git a/homeassistant/components/uptime_kuma/coordinator.py b/homeassistant/components/uptime_kuma/coordinator.py index 58eed420fd8..df64b12f8e9 100644 --- a/homeassistant/components/uptime_kuma/coordinator.py +++ b/homeassistant/components/uptime_kuma/coordinator.py @@ -104,7 +104,12 @@ def async_migrate_entities_unique_ids( f"{registry_entry.config_entry_id}_" ).removesuffix(f"_{registry_entry.translation_key}") if monitor := next( - (m for m in metrics.values() if m.monitor_name == name), None + ( + m + for m in metrics.values() + if m.monitor_name == name and m.monitor_id is not None + ), + None, ): entity_registry.async_update_entity( registry_entry.entity_id, diff --git a/homeassistant/components/uptime_kuma/manifest.json b/homeassistant/components/uptime_kuma/manifest.json index 42fac89a976..6ea7150f15d 100644 --- a/homeassistant/components/uptime_kuma/manifest.json +++ b/homeassistant/components/uptime_kuma/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/uptime_kuma", "iot_class": "cloud_polling", "loggers": ["pythonkuma"], - "quality_scale": "bronze", + "quality_scale": "platinum", "requirements": ["pythonkuma==0.3.1"] } diff --git a/homeassistant/components/uptime_kuma/quality_scale.yaml b/homeassistant/components/uptime_kuma/quality_scale.yaml index 3c9b5a3af50..56274d868ae 100644 --- a/homeassistant/components/uptime_kuma/quality_scale.yaml +++ b/homeassistant/components/uptime_kuma/quality_scale.yaml @@ -49,14 +49,14 @@ rules: status: done comment: hassio addon supports discovery, other installation methods are not discoverable docs-data-update: done - docs-examples: todo + docs-examples: done docs-known-limitations: done docs-supported-devices: status: exempt comment: integration is a service docs-supported-functions: done - docs-troubleshooting: todo - docs-use-cases: todo + docs-troubleshooting: done + docs-use-cases: done dynamic-devices: done entity-category: done entity-device-class: done diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 11db9108db3..081b7a15995 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -79,7 +79,10 @@ DEFAULT_NAME = "Vacuum cleaner robot" _DEPRECATED_STATE_IDLE = DeprecatedConstantEnum(VacuumActivity.IDLE, "2026.1") _DEPRECATED_STATE_PAUSED = DeprecatedConstantEnum(VacuumActivity.PAUSED, "2026.1") -_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ("template",) +_BATTERY_DEPRECATION_IGNORED_PLATFORMS = ( + "mqtt", + "template", +) class VacuumEntityFeature(IntFlag): @@ -333,7 +336,7 @@ class StateVacuumEntity( f"is setting the {property} which has been deprecated." f" Integration {self.platform.platform_name} should implement a sensor" " instead with a correct device class and link it to the same device", - core_integration_behavior=ReportBehavior.LOG, + core_integration_behavior=ReportBehavior.IGNORE, custom_integration_behavior=ReportBehavior.LOG, breaks_in_ha_version="2026.8", integration_domain=self.platform.platform_name, @@ -358,7 +361,7 @@ class StateVacuumEntity( f" Integration {self.platform.platform_name} should remove this as part of migrating" " the battery level and icon to a sensor", core_behavior=ReportBehavior.LOG, - core_integration_behavior=ReportBehavior.LOG, + core_integration_behavior=ReportBehavior.IGNORE, custom_integration_behavior=ReportBehavior.LOG, breaks_in_ha_version="2026.8", integration_domain=self.platform.platform_name, diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index d64a1361987..2b4b71bcf18 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -14,7 +14,7 @@ "velbus-protocol" ], "quality_scale": "bronze", - "requirements": ["velbus-aio==2025.5.0"], + "requirements": ["velbus-aio==2025.8.0"], "usb": [ { "vid": "10CF", diff --git a/homeassistant/components/vera/entity.py b/homeassistant/components/vera/entity.py index b3013c288c1..985761f2e63 100644 --- a/homeassistant/components/vera/entity.py +++ b/homeassistant/components/vera/entity.py @@ -48,6 +48,10 @@ class VeraEntity[_DeviceTypeT: veraApi.VeraDevice](Entity): """Subscribe to updates.""" self.controller.register(self.vera_device, self._update_callback) + async def async_will_remove_from_hass(self) -> None: + """Unsubscribe from updates.""" + self.controller.unregister(self.vera_device, self._update_callback) + def _update_callback(self, _device: _DeviceTypeT) -> None: """Update the state.""" self.schedule_update_ha_state(True) diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py index 57d39151160..35c32ab2af3 100644 --- a/homeassistant/components/vodafone_station/coordinator.py +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -187,4 +187,5 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]): model=sensors_data.get("sys_model_name"), hw_version=sensors_data["sys_hardware_version"], sw_version=sensors_data["sys_firmware_version"], + serial_number=self.serial_number, ) diff --git a/homeassistant/components/voip/assist_satellite.py b/homeassistant/components/voip/assist_satellite.py index ac8065cabf7..8d11cf2ff89 100644 --- a/homeassistant/components/voip/assist_satellite.py +++ b/homeassistant/components/voip/assist_satellite.py @@ -364,6 +364,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol if self._check_hangup_task is not None: self._check_hangup_task.cancel() self._check_hangup_task = None + self._rtp_port = None def connection_made(self, transport): """Server is ready.""" diff --git a/homeassistant/components/volvo/config_flow.py b/homeassistant/components/volvo/config_flow.py index f187d751a2d..0ae0e54077e 100644 --- a/homeassistant/components/volvo/config_flow.py +++ b/homeassistant/components/volvo/config_flow.py @@ -69,7 +69,7 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create an entry for the flow.""" - self._config_data |= data + self._config_data |= (self.init_data or {}) | data return await self.async_step_api_key() async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult: @@ -77,7 +77,7 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self.async_step_reauth_confirm() async def async_step_reconfigure( - self, _: dict[str, Any] | None = None + self, data: dict[str, Any] | None = None ) -> ConfigFlowResult: """Reconfigure the entry.""" return await self.async_step_api_key() @@ -121,7 +121,7 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): if user_input is None: if self.source == SOURCE_REAUTH: - user_input = self._config_data = dict(self._get_reauth_entry().data) + user_input = self._config_data api = _create_volvo_cars_api( self.hass, self._config_data[CONF_TOKEN][CONF_ACCESS_TOKEN], diff --git a/homeassistant/components/volvo/coordinator.py b/homeassistant/components/volvo/coordinator.py index 8ddaaee0781..d6c8f349a52 100644 --- a/homeassistant/components/volvo/coordinator.py +++ b/homeassistant/components/volvo/coordinator.py @@ -15,6 +15,7 @@ from volvocarsapi.models import ( VolvoAuthException, VolvoCarsApiBaseModel, VolvoCarsValue, + VolvoCarsValueStatusField, VolvoCarsVehicle, ) @@ -36,6 +37,16 @@ type VolvoConfigEntry = ConfigEntry[tuple[VolvoBaseCoordinator, ...]] type CoordinatorData = dict[str, VolvoCarsApiBaseModel | None] +def _is_invalid_api_field(field: VolvoCarsApiBaseModel | None) -> bool: + if not field: + return True + + if isinstance(field, VolvoCarsValueStatusField) and field.status == "ERROR": + return True + + return False + + class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]): """Volvo base coordinator.""" @@ -121,7 +132,13 @@ class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]): translation_key="update_failed", ) from result - data |= cast(CoordinatorData, result) + api_data = cast(CoordinatorData, result) + data |= { + key: field + for key, field in api_data.items() + if not _is_invalid_api_field(field) + } + valid = True # Raise an error if not a single API call succeeded @@ -243,6 +260,8 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator): "Volvo medium interval coordinator", ) + self._supported_capabilities: list[str] = [] + async def _async_determine_api_calls( self, ) -> list[Callable[[], Coroutine[Any, Any, Any]]]: @@ -250,6 +269,31 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator): capabilities = await self.api.async_get_energy_capabilities() if capabilities.get("isSupported", False): - return [self.api.async_get_energy_state] + self._supported_capabilities = [ + key + for key, value in capabilities.items() + if isinstance(value, dict) and value.get("isSupported", False) + ] + + return [self._async_get_energy_state] return [] + + async def _async_get_energy_state( + self, + ) -> dict[str, VolvoCarsValueStatusField | None]: + def _mark_ok( + field: VolvoCarsValueStatusField | None, + ) -> VolvoCarsValueStatusField | None: + if field: + field.status = "OK" + + return field + + energy_state = await self.api.async_get_energy_state() + + return { + key: _mark_ok(value) + for key, value in energy_state.items() + if key in self._supported_capabilities + } diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index dd982238a47..b9a620d898d 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass, replace +from dataclasses import dataclass import logging from typing import Any, cast @@ -47,7 +47,6 @@ _LOGGER = logging.getLogger(__name__) class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription): """Describes a Volvo sensor entity.""" - source_fields: list[str] | None = None value_fn: Callable[[VolvoCarsValue], Any] | None = None @@ -68,8 +67,8 @@ def _calculate_time_to_service(field: VolvoCarsValue) -> int: def _charging_power_value(field: VolvoCarsValue) -> int: return ( - int(field.value) - if isinstance(field, VolvoCarsValueStatusField) and field.status == "OK" + field.value + if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int) else 0 ) @@ -87,7 +86,12 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None: return None -_CHARGING_POWER_STATUS_OPTIONS = ["providing_power", "no_power_available"] +_CHARGING_POWER_STATUS_OPTIONS = [ + "fault", + "power_available_but_not_activated", + "providing_power", + "no_power_available", +] _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( # command-accessibility endpoint @@ -103,6 +107,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( "power_saving_mode", ], value_fn=_availability_status, + entity_category=EntityCategory.DIAGNOSTIC, ), # statistics endpoint VolvoSensorDescription( @@ -110,6 +115,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( api_field="averageEnergyConsumption", native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -117,6 +123,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( api_field="averageEnergyConsumptionAutomatic", native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -124,6 +131,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( api_field="averageEnergyConsumptionSinceCharge", native_unit_of_measurement=UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -131,6 +139,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( api_field="averageFuelConsumption", native_unit_of_measurement="L/100 km", state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -138,6 +147,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( api_field="averageFuelConsumptionAutomatic", native_unit_of_measurement="L/100 km", state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=1, ), # statistics endpoint VolvoSensorDescription( @@ -235,11 +245,15 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = ( "none", ], ), - # statistics & energy state endpoint + # statistics endpoint + # We're not using `electricRange` from the energy state endpoint because + # the official app seems to use `distanceToEmptyBattery`. + # In issue #150213, a user described to behavior as follows: + # - For a `distanceToEmptyBattery` of 250km, the `electricRange` was 150mi + # - For a `distanceToEmptyBattery` of 260km, the `electricRange` was 160mi VolvoSensorDescription( key="distance_to_empty_battery", - api_field="", - source_fields=["distanceToEmptyBattery", "electricRange"], + api_field="distanceToEmptyBattery", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, @@ -357,12 +371,7 @@ async def async_setup_entry( if description.key in added_keys: continue - if description.source_fields: - for field in description.source_fields: - if field in coordinator.data: - description = replace(description, api_field=field) - _add_entity(coordinator, description) - elif description.api_field in coordinator.data: + if description.api_field in coordinator.data: _add_entity(coordinator, description) async_add_entities(entities) diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index 4fe7429117c..c429c106574 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -94,7 +94,7 @@ "state": { "connected": "[%key:common::state::connected%]", "disconnected": "[%key:common::state::disconnected%]", - "fault": "[%key:common::state::error%]" + "fault": "[%key:common::state::fault%]" } }, "charging_current_limit": { @@ -106,6 +106,8 @@ "charging_power_status": { "name": "Charging power status", "state": { + "fault": "[%key:common::state::fault%]", + "power_available_but_not_activated": "Power available", "providing_power": "Providing power", "no_power_available": "No power" } diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 3a91690ef07..2e719a41a21 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,15 +1,13 @@ """The waze_travel_time component.""" import asyncio -from collections.abc import Collection import logging -from typing import Literal -from pywaze.route_calculator import CalcRoutesResponse, WazeRouteCalculator, WRCError +from pywaze.route_calculator import WazeRouteCalculator import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_REGION, Platform, UnitOfLength +from homeassistant.const import CONF_REGION, Platform from homeassistant.core import ( HomeAssistant, ServiceCall, @@ -27,7 +25,6 @@ from homeassistant.helpers.selector import ( TextSelectorConfig, TextSelectorType, ) -from homeassistant.util.unit_conversion import DistanceConverter from .const import ( CONF_AVOID_FERRIES, @@ -43,13 +40,13 @@ from .const import ( DEFAULT_FILTER, DEFAULT_VEHICLE_TYPE, DOMAIN, - IMPERIAL_UNITS, METRIC_UNITS, REGIONS, SEMAPHORE, UNITS, VEHICLE_TYPES, ) +from .coordinator import WazeTravelTimeCoordinator, async_get_travel_times PLATFORMS = [Platform.SENSOR] @@ -109,6 +106,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if SEMAPHORE not in hass.data.setdefault(DOMAIN, {}): hass.data.setdefault(DOMAIN, {})[SEMAPHORE] = asyncio.Semaphore(1) + httpx_client = get_async_client(hass) + client = WazeRouteCalculator( + region=config_entry.data[CONF_REGION].upper(), client=httpx_client + ) + + coordinator = WazeTravelTimeCoordinator(hass, config_entry, client) + config_entry.runtime_data = coordinator + + await coordinator.async_config_entry_first_refresh() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async def async_get_travel_times_service(service: ServiceCall) -> ServiceResponse: @@ -140,7 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b incl_filters=service.data.get(CONF_INCL_FILTER, DEFAULT_FILTER), excl_filters=service.data.get(CONF_EXCL_FILTER, DEFAULT_FILTER), ) - return {"routes": [vars(route) for route in response]} if response else None + return {"routes": [vars(route) for route in response]} hass.services.async_register( DOMAIN, @@ -152,106 +159,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_get_travel_times( - client: WazeRouteCalculator, - origin: str, - destination: str, - vehicle_type: str, - avoid_toll_roads: bool, - avoid_subscription_roads: bool, - avoid_ferries: bool, - realtime: bool, - units: Literal["metric", "imperial"] = "metric", - incl_filters: Collection[str] | None = None, - excl_filters: Collection[str] | None = None, -) -> list[CalcRoutesResponse] | None: - """Get all available routes.""" - - incl_filters = incl_filters or () - excl_filters = excl_filters or () - - _LOGGER.debug( - "Getting update for origin: %s destination: %s", - origin, - destination, - ) - routes = [] - vehicle_type = "" if vehicle_type.upper() == "CAR" else vehicle_type.upper() - try: - routes = await client.calc_routes( - origin, - destination, - vehicle_type=vehicle_type, - avoid_toll_roads=avoid_toll_roads, - avoid_subscription_roads=avoid_subscription_roads, - avoid_ferries=avoid_ferries, - real_time=realtime, - alternatives=3, - ) - _LOGGER.debug("Got routes: %s", routes) - - incl_routes: list[CalcRoutesResponse] = [] - - def should_include_route(route: CalcRoutesResponse) -> bool: - if len(incl_filters) < 1: - return True - should_include = any( - street_name in incl_filters or "" in incl_filters - for street_name in route.street_names - ) - if not should_include: - _LOGGER.debug( - "Excluding route [%s], because no inclusive filter matched any streetname", - route.name, - ) - return False - return True - - incl_routes = [route for route in routes if should_include_route(route)] - - filtered_routes: list[CalcRoutesResponse] = [] - - def should_exclude_route(route: CalcRoutesResponse) -> bool: - for street_name in route.street_names: - for excl_filter in excl_filters: - if excl_filter == street_name: - _LOGGER.debug( - "Excluding route, because exclusive filter [%s] matched streetname: %s", - excl_filter, - route.name, - ) - return True - return False - - filtered_routes = [ - route for route in incl_routes if not should_exclude_route(route) - ] - - if units == IMPERIAL_UNITS: - filtered_routes = [ - CalcRoutesResponse( - name=route.name, - distance=DistanceConverter.convert( - route.distance, UnitOfLength.KILOMETERS, UnitOfLength.MILES - ), - duration=route.duration, - street_names=route.street_names, - ) - for route in filtered_routes - if route.distance is not None - ] - - if len(filtered_routes) < 1: - _LOGGER.warning("No routes found") - return None - except WRCError as exp: - _LOGGER.warning("Error on retrieving data: %s", exp) - return None - - else: - return filtered_routes - - async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/waze_travel_time/coordinator.py b/homeassistant/components/waze_travel_time/coordinator.py new file mode 100644 index 00000000000..23dfea86ed2 --- /dev/null +++ b/homeassistant/components/waze_travel_time/coordinator.py @@ -0,0 +1,245 @@ +"""The Waze Travel Time data coordinator.""" + +import asyncio +from collections.abc import Collection +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import Literal + +from pywaze.route_calculator import CalcRoutesResponse, WazeRouteCalculator, WRCError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfLength +from homeassistant.core import HomeAssistant +from homeassistant.helpers.location import find_coordinates +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.unit_conversion import DistanceConverter + +from .const import ( + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + CONF_DESTINATION, + CONF_EXCL_FILTER, + CONF_INCL_FILTER, + CONF_ORIGIN, + CONF_REALTIME, + CONF_UNITS, + CONF_VEHICLE_TYPE, + DOMAIN, + IMPERIAL_UNITS, + SEMAPHORE, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=5) + +SECONDS_BETWEEN_API_CALLS = 0.5 + + +async def async_get_travel_times( + client: WazeRouteCalculator, + origin: str, + destination: str, + vehicle_type: str, + avoid_toll_roads: bool, + avoid_subscription_roads: bool, + avoid_ferries: bool, + realtime: bool, + units: Literal["metric", "imperial"] = "metric", + incl_filters: Collection[str] | None = None, + excl_filters: Collection[str] | None = None, +) -> list[CalcRoutesResponse]: + """Get all available routes.""" + + incl_filters = incl_filters or () + excl_filters = excl_filters or () + + _LOGGER.debug( + "Getting update for origin: %s destination: %s", + origin, + destination, + ) + routes = [] + vehicle_type = "" if vehicle_type.upper() == "CAR" else vehicle_type.upper() + try: + routes = await client.calc_routes( + origin, + destination, + vehicle_type=vehicle_type, + avoid_toll_roads=avoid_toll_roads, + avoid_subscription_roads=avoid_subscription_roads, + avoid_ferries=avoid_ferries, + real_time=realtime, + alternatives=3, + ) + + if len(routes) < 1: + _LOGGER.warning("No routes found") + return routes + + _LOGGER.debug("Got routes: %s", routes) + + incl_routes: list[CalcRoutesResponse] = [] + + def should_include_route(route: CalcRoutesResponse) -> bool: + if len(incl_filters) < 1: + return True + should_include = any( + street_name in incl_filters or "" in incl_filters + for street_name in route.street_names + ) + if not should_include: + _LOGGER.debug( + "Excluding route [%s], because no inclusive filter matched any streetname", + route.name, + ) + return False + return True + + incl_routes = [route for route in routes if should_include_route(route)] + + filtered_routes: list[CalcRoutesResponse] = [] + + def should_exclude_route(route: CalcRoutesResponse) -> bool: + for street_name in route.street_names: + for excl_filter in excl_filters: + if excl_filter == street_name: + _LOGGER.debug( + "Excluding route, because exclusive filter [%s] matched streetname: %s", + excl_filter, + route.name, + ) + return True + return False + + filtered_routes = [ + route for route in incl_routes if not should_exclude_route(route) + ] + + if len(filtered_routes) < 1: + _LOGGER.warning("No routes matched your filters") + return filtered_routes + + if units == IMPERIAL_UNITS: + filtered_routes = [ + CalcRoutesResponse( + name=route.name, + distance=DistanceConverter.convert( + route.distance, UnitOfLength.KILOMETERS, UnitOfLength.MILES + ), + duration=route.duration, + street_names=route.street_names, + ) + for route in filtered_routes + if route.distance is not None + ] + + except WRCError as exp: + raise UpdateFailed(f"Error on retrieving data: {exp}") from exp + + else: + return filtered_routes + + +@dataclass +class WazeTravelTimeData: + """WazeTravelTime data class.""" + + origin: str + destination: str + duration: float | None + distance: float | None + route: str | None + + +class WazeTravelTimeCoordinator(DataUpdateCoordinator[WazeTravelTimeData]): + """Waze Travel Time DataUpdateCoordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + client: WazeRouteCalculator, + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + config_entry=config_entry, + update_interval=SCAN_INTERVAL, + ) + self.client = client + self._origin = config_entry.data[CONF_ORIGIN] + self._destination = config_entry.data[CONF_DESTINATION] + + async def _async_update_data(self) -> WazeTravelTimeData: + """Get the latest data from Waze.""" + origin_coordinates = find_coordinates(self.hass, self._origin) + destination_coordinates = find_coordinates(self.hass, self._destination) + + _LOGGER.debug( + "Fetching Route for %s, from %s to %s", + self.config_entry.title, + self._origin, + self._destination, + ) + await self.hass.data[DOMAIN][SEMAPHORE].acquire() + try: + if origin_coordinates is None or destination_coordinates is None: + raise UpdateFailed("Unable to determine origin or destination") + + # Grab options on every update + incl_filter = self.config_entry.options[CONF_INCL_FILTER] + excl_filter = self.config_entry.options[CONF_EXCL_FILTER] + realtime = self.config_entry.options[CONF_REALTIME] + vehicle_type = self.config_entry.options[CONF_VEHICLE_TYPE] + avoid_toll_roads = self.config_entry.options[CONF_AVOID_TOLL_ROADS] + avoid_subscription_roads = self.config_entry.options[ + CONF_AVOID_SUBSCRIPTION_ROADS + ] + avoid_ferries = self.config_entry.options[CONF_AVOID_FERRIES] + routes = await async_get_travel_times( + self.client, + origin_coordinates, + destination_coordinates, + vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, + realtime, + self.config_entry.options[CONF_UNITS], + incl_filter, + excl_filter, + ) + if len(routes) < 1: + travel_data = WazeTravelTimeData( + origin=origin_coordinates, + destination=destination_coordinates, + duration=None, + distance=None, + route=None, + ) + + else: + route = routes[0] + + travel_data = WazeTravelTimeData( + origin=origin_coordinates, + destination=destination_coordinates, + duration=route.duration, + distance=route.distance, + route=route.name, + ) + + await asyncio.sleep(SECONDS_BETWEEN_API_CALLS) + + finally: + self.hass.data[DOMAIN][SEMAPHORE].release() + + return travel_data diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 1f21cc2ea78..c1323ce9397 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -2,56 +2,22 @@ from __future__ import annotations -import asyncio -from datetime import timedelta -import logging from typing import Any -import httpx -from pywaze.route_calculator import WazeRouteCalculator - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_NAME, - CONF_REGION, - EVENT_HOMEASSISTANT_STARTED, - UnitOfTime, -) -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.const import CONF_NAME, UnitOfTime +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.location import find_coordinates +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import async_get_travel_times -from .const import ( - CONF_AVOID_FERRIES, - CONF_AVOID_SUBSCRIPTION_ROADS, - CONF_AVOID_TOLL_ROADS, - CONF_DESTINATION, - CONF_EXCL_FILTER, - CONF_INCL_FILTER, - CONF_ORIGIN, - CONF_REALTIME, - CONF_UNITS, - CONF_VEHICLE_TYPE, - DEFAULT_NAME, - DOMAIN, - SEMAPHORE, -) - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(minutes=5) - -PARALLEL_UPDATES = 1 - -SECONDS_BETWEEN_API_CALLS = 0.5 +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import WazeTravelTimeCoordinator async def async_setup_entry( @@ -60,27 +26,20 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up a Waze travel time sensor entry.""" - destination = config_entry.data[CONF_DESTINATION] - origin = config_entry.data[CONF_ORIGIN] - region = config_entry.data[CONF_REGION] name = config_entry.data.get(CONF_NAME, DEFAULT_NAME) + coordinator = config_entry.runtime_data - data = WazeTravelTimeData( - region, - get_async_client(hass), - config_entry, - ) - - sensor = WazeTravelTime(config_entry.entry_id, name, origin, destination, data) + sensor = WazeTravelTimeSensor(config_entry.entry_id, name, coordinator) async_add_entities([sensor], False) -class WazeTravelTime(SensorEntity): +class WazeTravelTimeSensor(CoordinatorEntity[WazeTravelTimeCoordinator], SensorEntity): """Representation of a Waze travel time sensor.""" _attr_attribution = "Powered by Waze" _attr_native_unit_of_measurement = UnitOfTime.MINUTES + _attr_suggested_display_precision = 0 _attr_device_class = SensorDeviceClass.DURATION _attr_state_class = SensorStateClass.MEASUREMENT _attr_device_info = DeviceInfo( @@ -95,119 +54,30 @@ class WazeTravelTime(SensorEntity): self, unique_id: str, name: str, - origin: str, - destination: str, - waze_data: WazeTravelTimeData, + coordinator: WazeTravelTimeCoordinator, ) -> None: """Initialize the Waze travel time sensor.""" + super().__init__(coordinator) self._attr_unique_id = unique_id - self._waze_data = waze_data self._attr_name = name - self._origin = origin - self._destination = destination - self._state = None - - async def async_added_to_hass(self) -> None: - """Handle when entity is added.""" - if self.hass.state is not CoreState.running: - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, self.first_update - ) - else: - await self.first_update() @property def native_value(self) -> float | None: """Return the state of the sensor.""" - if self._waze_data.duration is not None: - return round(self._waze_data.duration) - + if self.coordinator.data is not None: + return self.coordinator.data.duration return None @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the last update.""" - if self._waze_data.duration is None: + if self.coordinator.data is None: return None return { - "duration": self._waze_data.duration, - "distance": self._waze_data.distance, - "route": self._waze_data.route, - "origin": self._waze_data.origin, - "destination": self._waze_data.destination, + "duration": self.coordinator.data.duration, + "distance": self.coordinator.data.distance, + "route": self.coordinator.data.route, + "origin": self.coordinator.data.origin, + "destination": self.coordinator.data.destination, } - - async def first_update(self, _=None) -> None: - """Run first update and write state.""" - await self.async_update() - self.async_write_ha_state() - - async def async_update(self) -> None: - """Fetch new state data for the sensor.""" - _LOGGER.debug("Fetching Route for %s", self._attr_name) - self._waze_data.origin = find_coordinates(self.hass, self._origin) - self._waze_data.destination = find_coordinates(self.hass, self._destination) - await self.hass.data[DOMAIN][SEMAPHORE].acquire() - try: - await self._waze_data.async_update() - await asyncio.sleep(SECONDS_BETWEEN_API_CALLS) - finally: - self.hass.data[DOMAIN][SEMAPHORE].release() - - -class WazeTravelTimeData: - """WazeTravelTime Data object.""" - - def __init__( - self, region: str, client: httpx.AsyncClient, config_entry: ConfigEntry - ) -> None: - """Set up WazeRouteCalculator.""" - self.config_entry = config_entry - self.client = WazeRouteCalculator(region=region, client=client) - self.origin: str | None = None - self.destination: str | None = None - self.duration = None - self.distance = None - self.route = None - - async def async_update(self): - """Update WazeRouteCalculator Sensor.""" - _LOGGER.debug( - "Getting update for origin: %s destination: %s", - self.origin, - self.destination, - ) - if self.origin is not None and self.destination is not None: - # Grab options on every update - incl_filter = self.config_entry.options[CONF_INCL_FILTER] - excl_filter = self.config_entry.options[CONF_EXCL_FILTER] - realtime = self.config_entry.options[CONF_REALTIME] - vehicle_type = self.config_entry.options[CONF_VEHICLE_TYPE] - avoid_toll_roads = self.config_entry.options[CONF_AVOID_TOLL_ROADS] - avoid_subscription_roads = self.config_entry.options[ - CONF_AVOID_SUBSCRIPTION_ROADS - ] - avoid_ferries = self.config_entry.options[CONF_AVOID_FERRIES] - routes = await async_get_travel_times( - self.client, - self.origin, - self.destination, - vehicle_type, - avoid_toll_roads, - avoid_subscription_roads, - avoid_ferries, - realtime, - self.config_entry.options[CONF_UNITS], - incl_filter, - excl_filter, - ) - if routes: - route = routes[0] - else: - _LOGGER.warning("No routes found") - return - - self.duration = route.duration - self.distance = route.distance - self.route = route.name diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index c3c3e9a564f..f8201fe3bef 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/webostv", "iot_class": "local_push", "loggers": ["aiowebostv"], - "requirements": ["aiowebostv==0.7.4"], + "requirements": ["aiowebostv==0.7.5"], "ssdp": [ { "st": "urn:lge-com:service:webos-second-screen:1" diff --git a/homeassistant/components/wemo/coordinator.py b/homeassistant/components/wemo/coordinator.py index 6cda83f6419..cb3c8a558b6 100644 --- a/homeassistant/components/wemo/coordinator.py +++ b/homeassistant/components/wemo/coordinator.py @@ -102,7 +102,6 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): name=wemo.name, update_interval=timedelta(seconds=30), ) - self.hass = hass self.wemo = wemo self.device_id: str | None = None self.device_info = _create_device_info(wemo) diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py index d8b59075368..dd154488be2 100644 --- a/homeassistant/components/withings/diagnostics.py +++ b/homeassistant/components/withings/diagnostics.py @@ -2,16 +2,23 @@ from __future__ import annotations +from dataclasses import asdict from typing import Any from yarl import URL +from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.webhook import async_generate_url as webhook_generate_url from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from . import CONF_CLOUDHOOK_URL, WithingsConfigEntry +TO_REDACT = { + "device_id", + "hashed_device_id", +} + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: WithingsConfigEntry @@ -53,4 +60,8 @@ async def async_get_config_entry_diagnostics( "received_sleep_data": withings_data.sleep_coordinator.data is not None, "received_workout_data": withings_data.workout_coordinator.data is not None, "received_activity_data": withings_data.activity_coordinator.data is not None, + "devices": async_redact_data( + [asdict(v) for v in withings_data.device_coordinator.data.values()], + TO_REDACT, + ), } diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index 0df4224a4ca..cbcf12cf31c 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -2,103 +2,72 @@ from __future__ import annotations -from functools import partial +from datetime import timedelta +from typing import cast -from holidays import HolidayBase, country_holidays +from holidays import DateLike, HolidayBase from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.setup import SetupPhases, async_pause_setup +from homeassistant.util import dt as dt_util -from .const import CONF_PROVINCE, DOMAIN, PLATFORMS +from .const import ( + CONF_ADD_HOLIDAYS, + CONF_CATEGORY, + CONF_OFFSET, + CONF_PROVINCE, + CONF_REMOVE_HOLIDAYS, + LOGGER, + PLATFORMS, +) +from .util import ( + add_remove_custom_holidays, + async_validate_country_and_province, + get_holidays_object, + validate_dates, +) + +type WorkdayConfigEntry = ConfigEntry[HolidayBase] -async def _async_validate_country_and_province( - hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None -) -> None: - """Validate country and province.""" - - if not country: - return - try: - with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): - # import executor job is used here because multiple integrations use - # the holidays library and it is not thread safe to import it in parallel - # https://github.com/python/cpython/issues/83065 - await hass.async_add_import_executor_job(country_holidays, country) - except NotImplementedError as ex: - async_create_issue( - hass, - DOMAIN, - "bad_country", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.ERROR, - translation_key="bad_country", - translation_placeholders={"title": entry.title}, - data={"entry_id": entry.entry_id, "country": None}, - ) - raise ConfigEntryError(f"Selected country {country} is not valid") from ex - - if not province: - return - try: - with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): - # import executor job is used here because multiple integrations use - # the holidays library and it is not thread safe to import it in parallel - # https://github.com/python/cpython/issues/83065 - await hass.async_add_import_executor_job( - partial(country_holidays, country, subdiv=province) - ) - except NotImplementedError as ex: - async_create_issue( - hass, - DOMAIN, - "bad_province", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.ERROR, - translation_key="bad_province", - translation_placeholders={ - CONF_COUNTRY: country, - "title": entry.title, - }, - data={"entry_id": entry.entry_id, "country": country}, - ) - raise ConfigEntryError( - f"Selected province {province} for country {country} is not valid" - ) from ex - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: WorkdayConfigEntry) -> bool: """Set up Workday from a config entry.""" + calc_add_holidays = cast( + list[DateLike], validate_dates(entry.options[CONF_ADD_HOLIDAYS]) + ) + calc_remove_holidays: list[str] = validate_dates( + entry.options[CONF_REMOVE_HOLIDAYS] + ) + categories: list[str] | None = entry.options.get(CONF_CATEGORY) country: str | None = entry.options.get(CONF_COUNTRY) + days_offset: int = int(entry.options[CONF_OFFSET]) + language: str | None = entry.options.get(CONF_LANGUAGE) province: str | None = entry.options.get(CONF_PROVINCE) + year: int = (dt_util.now() + timedelta(days=days_offset)).year - await _async_validate_country_and_province(hass, entry, country, province) + await async_validate_country_and_province(hass, entry, country, province) - if country and CONF_LANGUAGE not in entry.options: - with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): - # import executor job is used here because multiple integrations use - # the holidays library and it is not thread safe to import it in parallel - # https://github.com/python/cpython/issues/83065 - cls: HolidayBase = await hass.async_add_import_executor_job( - partial(country_holidays, country, subdiv=province) - ) - default_language = cls.default_language - new_options = entry.options.copy() - new_options[CONF_LANGUAGE] = default_language - hass.config_entries.async_update_entry(entry, options=new_options) + entry.runtime_data = await hass.async_add_executor_job( + get_holidays_object, country, province, year, language, categories + ) + + add_remove_custom_holidays( + hass, entry, country, calc_add_holidays, calc_remove_holidays + ) + + LOGGER.debug("Found the following holidays for your configuration:") + for holiday_date, name in sorted(entry.runtime_data.items()): + # Make explicit str variable to avoid "Incompatible types in assignment" + _holiday_string = holiday_date.strftime("%Y-%m-%d") + LOGGER.debug("%s %s", _holiday_string, name) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: WorkdayConfigEntry) -> bool: """Unload Workday config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index a48e19e59b2..69bdd315609 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -2,249 +2,40 @@ from __future__ import annotations -from datetime import date, datetime, timedelta +from datetime import datetime from typing import Final -from holidays import ( - PUBLIC, - HolidayBase, - __version__ as python_holidays_version, - country_holidays, -) +from holidays import HolidayBase import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME -from homeassistant.core import ( - CALLBACK_TYPE, - HomeAssistant, - ServiceResponse, - SupportsResponse, - callback, -) +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, async_get_current_platform, ) -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.util import dt as dt_util, slugify -from .const import ( - ALLOWED_DAYS, - CONF_ADD_HOLIDAYS, - CONF_CATEGORY, - CONF_EXCLUDES, - CONF_OFFSET, - CONF_PROVINCE, - CONF_REMOVE_HOLIDAYS, - CONF_WORKDAYS, - DOMAIN, - LOGGER, -) +from . import WorkdayConfigEntry +from .const import CONF_EXCLUDES, CONF_OFFSET, CONF_WORKDAYS +from .entity import BaseWorkdayEntity SERVICE_CHECK_DATE: Final = "check_date" CHECK_DATE: Final = "check_date" -def validate_dates(holiday_list: list[str]) -> list[str]: - """Validate and adds to list of dates to add or remove.""" - calc_holidays: list[str] = [] - for add_date in holiday_list: - if add_date.find(",") > 0: - dates = add_date.split(",", maxsplit=1) - d1 = dt_util.parse_date(dates[0]) - d2 = dt_util.parse_date(dates[1]) - if d1 is None or d2 is None: - LOGGER.error("Incorrect dates in date range: %s", add_date) - continue - _range: timedelta = d2 - d1 - for i in range(_range.days + 1): - day: date = d1 + timedelta(days=i) - calc_holidays.append(day.strftime("%Y-%m-%d")) - continue - calc_holidays.append(add_date) - return calc_holidays - - -def _get_obj_holidays( - country: str | None, - province: str | None, - year: int, - language: str | None, - categories: list[str] | None, -) -> HolidayBase: - """Get the object for the requested country and year.""" - if not country: - return HolidayBase() - - set_categories = None - if categories: - category_list = [PUBLIC] - category_list.extend(categories) - set_categories = tuple(category_list) - - obj_holidays: HolidayBase = country_holidays( - country, - subdiv=province, - years=[year, year + 1], - language=language, - categories=set_categories, - ) - - supported_languages = obj_holidays.supported_languages - default_language = obj_holidays.default_language - - if default_language and not language: - # If no language is set, use the default language - LOGGER.debug("Changing language from None to %s", default_language) - return country_holidays( # Return default if no language - country, - subdiv=province, - years=year, - language=default_language, - categories=set_categories, - ) - - if ( - default_language - and language - and language not in supported_languages - and language.startswith("en") - ): - # If language does not match supported languages, use the first English variant - if default_language.startswith("en"): - LOGGER.debug("Changing language from %s to %s", language, default_language) - return country_holidays( # Return default English if default language - country, - subdiv=province, - years=year, - language=default_language, - categories=set_categories, - ) - for lang in supported_languages: - if lang.startswith("en"): - LOGGER.debug("Changing language from %s to %s", language, lang) - return country_holidays( - country, - subdiv=province, - years=year, - language=lang, - categories=set_categories, - ) - - if default_language and language and language not in supported_languages: - # If language does not match supported languages, use the default language - LOGGER.debug("Changing language from %s to %s", language, default_language) - return country_holidays( # Return default English if default language - country, - subdiv=province, - years=year, - language=default_language, - categories=set_categories, - ) - - return obj_holidays - - async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: WorkdayConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Workday sensor.""" - add_holidays: list[str] = entry.options[CONF_ADD_HOLIDAYS] - remove_holidays: list[str] = entry.options[CONF_REMOVE_HOLIDAYS] - country: str | None = entry.options.get(CONF_COUNTRY) days_offset: int = int(entry.options[CONF_OFFSET]) excludes: list[str] = entry.options[CONF_EXCLUDES] - province: str | None = entry.options.get(CONF_PROVINCE) sensor_name: str = entry.options[CONF_NAME] workdays: list[str] = entry.options[CONF_WORKDAYS] - language: str | None = entry.options.get(CONF_LANGUAGE) - categories: list[str] | None = entry.options.get(CONF_CATEGORY) - - year: int = (dt_util.now() + timedelta(days=days_offset)).year - obj_holidays: HolidayBase = await hass.async_add_executor_job( - _get_obj_holidays, country, province, year, language, categories - ) - calc_add_holidays: list[str] = validate_dates(add_holidays) - calc_remove_holidays: list[str] = validate_dates(remove_holidays) - next_year = dt_util.now().year + 1 - - # Add custom holidays - try: - obj_holidays.append(calc_add_holidays) # type: ignore[arg-type] - except ValueError as error: - LOGGER.error("Could not add custom holidays: %s", error) - - # Remove holidays - for remove_holiday in calc_remove_holidays: - try: - # is this formatted as a date? - if dt_util.parse_date(remove_holiday): - # remove holiday by date - removed = obj_holidays.pop(remove_holiday) - LOGGER.debug("Removed %s", remove_holiday) - else: - # remove holiday by name - LOGGER.debug("Treating '%s' as named holiday", remove_holiday) - removed = obj_holidays.pop_named(remove_holiday) - for holiday in removed: - LOGGER.debug("Removed %s by name '%s'", holiday, remove_holiday) - except KeyError as unmatched: - LOGGER.warning("No holiday found matching %s", unmatched) - if _date := dt_util.parse_date(remove_holiday): - if _date.year <= next_year: - # Only check and raise issues for current and next year - async_create_issue( - hass, - DOMAIN, - f"bad_date_holiday-{entry.entry_id}-{slugify(remove_holiday)}", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="bad_date_holiday", - translation_placeholders={ - CONF_COUNTRY: country if country else "-", - "title": entry.title, - CONF_REMOVE_HOLIDAYS: remove_holiday, - }, - data={ - "entry_id": entry.entry_id, - "country": country, - "named_holiday": remove_holiday, - }, - ) - else: - async_create_issue( - hass, - DOMAIN, - f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="bad_named_holiday", - translation_placeholders={ - CONF_COUNTRY: country if country else "-", - "title": entry.title, - CONF_REMOVE_HOLIDAYS: remove_holiday, - }, - data={ - "entry_id": entry.entry_id, - "country": country, - "named_holiday": remove_holiday, - }, - ) - - LOGGER.debug("Found the following holidays for your configuration:") - for holiday_date, name in sorted(obj_holidays.items()): - # Make explicit str variable to avoid "Incompatible types in assignment" - _holiday_string = holiday_date.strftime("%Y-%m-%d") - LOGGER.debug("%s %s", _holiday_string, name) + obj_holidays = entry.runtime_data platform = async_get_current_platform() platform.async_register_entity_service( @@ -269,14 +60,10 @@ async def async_setup_entry( ) -class IsWorkdaySensor(BinarySensorEntity): +class IsWorkdaySensor(BaseWorkdayEntity, BinarySensorEntity): """Implementation of a Workday sensor.""" - _attr_has_entity_name = True _attr_name = None - _attr_translation_key = DOMAIN - _attr_should_poll = False - unsub: CALLBACK_TYPE | None = None def __init__( self, @@ -288,87 +75,20 @@ class IsWorkdaySensor(BinarySensorEntity): entry_id: str, ) -> None: """Initialize the Workday sensor.""" - self._obj_holidays = obj_holidays - self._workdays = workdays - self._excludes = excludes - self._days_offset = days_offset + super().__init__( + obj_holidays, + workdays, + excludes, + days_offset, + name, + entry_id, + ) self._attr_extra_state_attributes = { CONF_WORKDAYS: workdays, CONF_EXCLUDES: excludes, CONF_OFFSET: days_offset, } - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="python-holidays", - model=python_holidays_version, - name=name, - ) - - def is_include(self, day: str, now: date) -> bool: - """Check if given day is in the includes list.""" - if day in self._workdays: - return True - if "holiday" in self._workdays and now in self._obj_holidays: - return True - - return False - - def is_exclude(self, day: str, now: date) -> bool: - """Check if given day is in the excludes list.""" - if day in self._excludes: - return True - if "holiday" in self._excludes and now in self._obj_holidays: - return True - - return False - - def get_next_interval(self, now: datetime) -> datetime: - """Compute next time an update should occur.""" - tomorrow = dt_util.as_local(now) + timedelta(days=1) - return dt_util.start_of_local_day(tomorrow) - - def _update_state_and_setup_listener(self) -> None: - """Update state and setup listener for next interval.""" - now = dt_util.now() - self.update_data(now) - self.unsub = async_track_point_in_utc_time( - self.hass, self.point_in_time_listener, self.get_next_interval(now) - ) - - @callback - def point_in_time_listener(self, time_date: datetime) -> None: - """Get the latest data and update state.""" - self._update_state_and_setup_listener() - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Set up first update.""" - self._update_state_and_setup_listener() def update_data(self, now: datetime) -> None: """Get date and look whether it is a holiday.""" self._attr_is_on = self.date_is_workday(now) - - def check_date(self, check_date: date) -> ServiceResponse: - """Service to check if date is workday or not.""" - return {"workday": self.date_is_workday(check_date)} - - def date_is_workday(self, check_date: date) -> bool: - """Check if date is workday.""" - # Default is no workday - is_workday = False - - # Get ISO day of the week (1 = Monday, 7 = Sunday) - adjusted_date = check_date + timedelta(days=self._days_offset) - day = adjusted_date.isoweekday() - 1 - day_of_week = ALLOWED_DAYS[day] - - if self.is_include(day_of_week, adjusted_date): - is_workday = True - - if self.is_exclude(day_of_week, adjusted_date): - is_workday = False - - return is_workday diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 1d91e1d5ae3..20d9040e527 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -86,6 +86,9 @@ def add_province_and_language_to_schema( SelectOptionDict(value=k, label=", ".join(v)) for k, v in subdiv_aliases.items() ] + for option in province_options: + if option["label"] == "": + option["label"] = option["value"] else: province_options = provinces province_schema = { diff --git a/homeassistant/components/workday/entity.py b/homeassistant/components/workday/entity.py new file mode 100644 index 00000000000..c75a4089ed2 --- /dev/null +++ b/homeassistant/components/workday/entity.py @@ -0,0 +1,115 @@ +"""Base workday entity.""" + +from __future__ import annotations + +from abc import abstractmethod +from datetime import date, datetime, timedelta + +from holidays import HolidayBase, __version__ as python_holidays_version + +from homeassistant.core import CALLBACK_TYPE, ServiceResponse, callback +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util import dt as dt_util + +from .const import ALLOWED_DAYS, DOMAIN + + +class BaseWorkdayEntity(Entity): + """Implementation of a base Workday entity.""" + + _attr_has_entity_name = True + _attr_translation_key = DOMAIN + _attr_should_poll = False + unsub: CALLBACK_TYPE | None = None + + def __init__( + self, + obj_holidays: HolidayBase, + workdays: list[str], + excludes: list[str], + days_offset: int, + name: str, + entry_id: str, + ) -> None: + """Initialize the Workday entity.""" + self._obj_holidays = obj_holidays + self._workdays = workdays + self._excludes = excludes + self._days_offset = days_offset + self._attr_unique_id = entry_id + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="python-holidays", + model=python_holidays_version, + name=name, + ) + + def is_include(self, day: str, now: date) -> bool: + """Check if given day is in the includes list.""" + if day in self._workdays: + return True + if "holiday" in self._workdays and now in self._obj_holidays: + return True + + return False + + def is_exclude(self, day: str, now: date) -> bool: + """Check if given day is in the excludes list.""" + if day in self._excludes: + return True + if "holiday" in self._excludes and now in self._obj_holidays: + return True + + return False + + def get_next_interval(self, now: datetime) -> datetime: + """Compute next time an update should occur.""" + tomorrow = dt_util.as_local(now) + timedelta(days=1) + return dt_util.start_of_local_day(tomorrow) + + def _update_state_and_setup_listener(self) -> None: + """Update state and setup listener for next interval.""" + now = dt_util.now() + self.update_data(now) + self.unsub = async_track_point_in_utc_time( + self.hass, self.point_in_time_listener, self.get_next_interval(now) + ) + + @callback + def point_in_time_listener(self, time_date: datetime) -> None: + """Get the latest data and update state.""" + self._update_state_and_setup_listener() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Set up first update.""" + self._update_state_and_setup_listener() + + @abstractmethod + def update_data(self, now: datetime) -> None: + """Update data.""" + + def check_date(self, check_date: date) -> ServiceResponse: + """Service to check if date is workday or not.""" + return {"workday": self.date_is_workday(check_date)} + + def date_is_workday(self, check_date: date) -> bool: + """Check if date is workday.""" + # Default is no workday + is_workday = False + + # Get ISO day of the week (1 = Monday, 7 = Sunday) + adjusted_date = check_date + timedelta(days=self._days_offset) + day = adjusted_date.isoweekday() - 1 + day_of_week = ALLOWED_DAYS[day] + + if self.is_include(day_of_week, adjusted_date): + is_workday = True + + if self.is_exclude(day_of_week, adjusted_date): + is_workday = False + + return is_workday diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index d2309702728..0e336632b2e 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.78"] + "requirements": ["holidays==0.79"] } diff --git a/homeassistant/components/workday/util.py b/homeassistant/components/workday/util.py new file mode 100644 index 00000000000..726563febaf --- /dev/null +++ b/homeassistant/components/workday/util.py @@ -0,0 +1,254 @@ +"""Helpers functions for the Workday component.""" + +from datetime import date, timedelta +from functools import partial +from typing import TYPE_CHECKING + +from holidays import PUBLIC, DateLike, HolidayBase, country_holidays + +from homeassistant.const import CONF_COUNTRY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.setup import SetupPhases, async_pause_setup +from homeassistant.util import dt as dt_util, slugify + +if TYPE_CHECKING: + from . import WorkdayConfigEntry +from .const import CONF_REMOVE_HOLIDAYS, DOMAIN, LOGGER + + +async def async_validate_country_and_province( + hass: HomeAssistant, + entry: "WorkdayConfigEntry", + country: str | None, + province: str | None, +) -> None: + """Validate country and province.""" + + if not country: + return + try: + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job(country_holidays, country) + except NotImplementedError as ex: + async_create_issue( + hass, + DOMAIN, + "bad_country", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="bad_country", + translation_placeholders={"title": entry.title}, + data={"entry_id": entry.entry_id, "country": None}, + ) + raise ConfigEntryError(f"Selected country {country} is not valid") from ex + + if not province: + return + try: + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) + except NotImplementedError as ex: + async_create_issue( + hass, + DOMAIN, + "bad_province", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.ERROR, + translation_key="bad_province", + translation_placeholders={ + CONF_COUNTRY: country, + "title": entry.title, + }, + data={"entry_id": entry.entry_id, "country": country}, + ) + raise ConfigEntryError( + f"Selected province {province} for country {country} is not valid" + ) from ex + + +def validate_dates(holiday_list: list[str]) -> list[str]: + """Validate and add to list of dates to add or remove.""" + calc_holidays: list[str] = [] + for add_date in holiday_list: + if add_date.find(",") > 0: + dates = add_date.split(",", maxsplit=1) + d1 = dt_util.parse_date(dates[0]) + d2 = dt_util.parse_date(dates[1]) + if d1 is None or d2 is None: + LOGGER.error("Incorrect dates in date range: %s", add_date) + continue + _range: timedelta = d2 - d1 + for i in range(_range.days + 1): + day: date = d1 + timedelta(days=i) + calc_holidays.append(day.strftime("%Y-%m-%d")) + continue + calc_holidays.append(add_date) + return calc_holidays + + +def get_holidays_object( + country: str | None, + province: str | None, + year: int, + language: str | None, + categories: list[str] | None, +) -> HolidayBase: + """Get the object for the requested country and year.""" + if not country: + return HolidayBase() + + set_categories = None + if categories: + category_list = [PUBLIC] + category_list.extend(categories) + set_categories = tuple(category_list) + + obj_holidays: HolidayBase = country_holidays( + country, + subdiv=province, + years=[year, year + 1], + language=language, + categories=set_categories, + ) + + supported_languages = obj_holidays.supported_languages + default_language = obj_holidays.default_language + + if default_language and not language: + # If no language is set, use the default language + LOGGER.debug("Changing language from None to %s", default_language) + return country_holidays( # Return default if no language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + + if ( + default_language + and language + and language not in supported_languages + and language.startswith("en") + ): + # If language does not match supported languages, use the first English variant + if default_language.startswith("en"): + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + for lang in supported_languages: + if lang.startswith("en"): + LOGGER.debug("Changing language from %s to %s", language, lang) + return country_holidays( + country, + subdiv=province, + years=year, + language=lang, + categories=set_categories, + ) + + if default_language and language and language not in supported_languages: + # If language does not match supported languages, use the default language + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + + return obj_holidays + + +def add_remove_custom_holidays( + hass: HomeAssistant, + entry: "WorkdayConfigEntry", + country: str | None, + calc_add_holidays: list[DateLike], + calc_remove_holidays: list[str], +) -> None: + """Add or remove custom holidays.""" + next_year = dt_util.now().year + 1 + + # Add custom holidays + try: + entry.runtime_data.append(calc_add_holidays) + except ValueError as error: + LOGGER.error("Could not add custom holidays: %s", error) + + # Remove custom holidays + for remove_holiday in calc_remove_holidays: + try: + # is this formatted as a date? + if dt_util.parse_date(remove_holiday): + # remove holiday by date + removed = entry.runtime_data.pop(remove_holiday) + LOGGER.debug("Removed %s", remove_holiday) + else: + # remove holiday by name + LOGGER.debug("Treating '%s' as named holiday", remove_holiday) + removed = entry.runtime_data.pop_named(remove_holiday) + for holiday in removed: + LOGGER.debug("Removed %s by name '%s'", holiday, remove_holiday) + except KeyError as unmatched: + LOGGER.warning("No holiday found matching %s", unmatched) + if _date := dt_util.parse_date(remove_holiday): + if _date.year <= next_year: + # Only check and raise issues for max next year + async_create_issue( + hass, + DOMAIN, + f"bad_date_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_date_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) + else: + async_create_issue( + hass, + DOMAIN, + f"bad_named_holiday-{entry.entry_id}-{slugify(remove_holiday)}", + is_fixable=True, + is_persistent=False, + severity=IssueSeverity.WARNING, + translation_key="bad_named_holiday", + translation_placeholders={ + CONF_COUNTRY: country if country else "-", + "title": entry.title, + CONF_REMOVE_HOLIDAYS: remove_holiday, + }, + data={ + "entry_id": entry.entry_id, + "country": country, + "named_holiday": remove_holiday, + }, + ) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index ffdd8f29a79..5f64cd1acdc 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -75,7 +75,7 @@ "lock_outside_the_door": "Lock outside the door", "unlock_outside_the_door": "Unlock outside the door", "lock_inside_the_door": "Lock inside the door", - "unlock_inside_the_door": "Unlock outside the door", + "unlock_inside_the_door": "Unlock inside the door", "locked": "Locked", "turn_on_antilock": "Turn on antilock", "release_the_antilock": "Release antilock", diff --git a/homeassistant/components/yale/manifest.json b/homeassistant/components/yale/manifest.json index 9086bb15575..0397fab7705 100644 --- a/homeassistant/components/yale/manifest.json +++ b/homeassistant/components/yale/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/yale", "iot_class": "cloud_push", "loggers": ["socketio", "engineio", "yalexs"], - "requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"] + "requirements": ["yalexs==8.12.0", "yalexs-ble==3.1.2"] } diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 96db2ab555a..f33da34c1fc 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -43,6 +43,7 @@ PLATFORMS = [ Platform.LIGHT, Platform.LOCK, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index 7d5323663de..2c914e84a08 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -5,13 +5,16 @@ from __future__ import annotations import asyncio from datetime import UTC, datetime, timedelta import logging +from typing import Any +from yolink.client_request import ClientRequest from yolink.device import YoLinkDevice from yolink.exception import YoLinkAuthFailError, YoLinkClientError +from yolink.model import BRDP from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ATTR_DEVICE_STATE, ATTR_LORA_INFO, DOMAIN, YOLINK_OFFLINE_TIME @@ -89,3 +92,16 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): self.dev_net_type = dev_lora_info.get("devNetType") return device_state return {} + + async def call_device(self, request: ClientRequest) -> dict[str, Any]: + """Call device api.""" + try: + # call_device will check result, fail by raise YoLinkClientError + resp: BRDP = await self.device.call_device(request) + except YoLinkAuthFailError as yl_auth_err: + self.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError(yl_auth_err) from yl_auth_err + except YoLinkClientError as yl_client_err: + raise HomeAssistantError(yl_client_err) from yl_client_err + else: + return resp.data diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 7828bf91541..ecc42ad1a0e 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -3,13 +3,12 @@ from __future__ import annotations from abc import abstractmethod +from typing import Any from yolink.client_request import ClientRequest -from yolink.exception import YoLinkAuthFailError, YoLinkClientError from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -64,13 +63,6 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): def update_entity_state(self, state: dict) -> None: """Parse and update entity state, should be overridden.""" - async def call_device(self, request: ClientRequest) -> None: + async def call_device(self, request: ClientRequest) -> dict[str, Any]: """Call device api.""" - try: - # call_device will check result, fail by raise YoLinkClientError - await self.coordinator.device.call_device(request) - except YoLinkAuthFailError as yl_auth_err: - self.config_entry.async_start_reauth(self.hass) - raise HomeAssistantError(yl_auth_err) from yl_auth_err - except YoLinkClientError as yl_client_err: - raise HomeAssistantError(yl_client_err) from yl_client_err + return await self.coordinator.call_device(request) diff --git a/homeassistant/components/yolink/icons.json b/homeassistant/components/yolink/icons.json index 6d9062a92b8..59366b804f5 100644 --- a/homeassistant/components/yolink/icons.json +++ b/homeassistant/components/yolink/icons.json @@ -27,10 +27,20 @@ "default": "mdi:gauge" } }, + "select": { + "sprinkler_mode": { + "default": "mdi:auto-mode" + } + }, "switch": { "manipulator_state": { "default": "mdi:pipe" } + }, + "valve": { + "sprinkler_valve": { + "default": "mdi:sprinkler-variant" + } } }, "services": { diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 89001f98c16..138667e7e73 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.5.7"] + "requirements": ["yolink-api==0.5.8"] } diff --git a/homeassistant/components/yolink/select.py b/homeassistant/components/yolink/select.py new file mode 100644 index 00000000000..e98b0440b92 --- /dev/null +++ b/homeassistant/components/yolink/select.py @@ -0,0 +1,119 @@ +"""YoLink select platform.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from yolink.client_request import ClientRequest +from yolink.const import ATTR_DEVICE_SPRINKLER +from yolink.device import YoLinkDevice +from yolink.message_resolver import sprinkler_message_resolve + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass(frozen=True, kw_only=True) +class YoLinkSelectEntityDescription(SelectEntityDescription): + """YoLink SelectEntityDescription.""" + + state_key: str = "state" + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + should_update_entity: Callable = lambda state: True + value: Callable = lambda data: data + on_option_selected: Callable[[YoLinkCoordinator, str], Awaitable[bool]] + + +async def set_sprinker_mode_fn(coordinator: YoLinkCoordinator, option: str) -> bool: + """Set sprinkler mode.""" + data: dict[str, Any] = await coordinator.call_device( + ClientRequest( + "setState", + { + "state": { + "mode": option, + } + }, + ) + ) + sprinkler_message_resolve(coordinator.device, data, None) + coordinator.async_set_updated_data(data) + return True + + +SELECTOR_MAPPINGS: tuple[YoLinkSelectEntityDescription, ...] = ( + YoLinkSelectEntityDescription( + key="model", + options=["auto", "manual", "off"], + translation_key="sprinkler_mode", + value=lambda data: ( + data.get("mode") if data is not None else None + ), # watering state report will missing state field + exists_fn=lambda device: device.device_type == ATTR_DEVICE_SPRINKLER, + should_update_entity=lambda value: value is not None, + on_option_selected=set_sprinker_mode_fn, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up YoLink select from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id].device_coordinators + async_add_entities( + YoLinkSelectEntity(config_entry, selector_device_coordinator, description) + for selector_device_coordinator in device_coordinators.values() + if selector_device_coordinator.device.device_type in [ATTR_DEVICE_SPRINKLER] + for description in SELECTOR_MAPPINGS + if description.exists_fn(selector_device_coordinator.device) + ) + + +class YoLinkSelectEntity(YoLinkEntity, SelectEntity): + """YoLink Select Entity.""" + + entity_description: YoLinkSelectEntityDescription + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + description: YoLinkSelectEntityDescription, + ) -> None: + """Init YoLink Select.""" + super().__init__(config_entry, coordinator) + self.entity_description = description + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + if ( + current_value := self.entity_description.value( + state.get(self.entity_description.state_key) + ) + ) is None and self.entity_description.should_update_entity( + current_value + ) is False: + return + self._attr_current_option = current_value + self.async_write_ha_state() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + if await self.entity_description.on_option_selected(self.coordinator, option): + self._attr_current_option = option + self.async_write_ha_state() diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 5425c242821..3bb0e965eae 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -23,6 +23,8 @@ from yolink.const import ( ATTR_DEVICE_SMART_REMOTER, ATTR_DEVICE_SMOKE_ALARM, ATTR_DEVICE_SOIL_TH_SENSOR, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ATTR_DEVICE_SWITCH, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_THERMOSTAT, @@ -110,6 +112,8 @@ SENSOR_DEVICE_TYPE = [ ATTR_GARAGE_DOOR_CONTROLLER, ATTR_DEVICE_SOIL_TH_SENSOR, ATTR_DEVICE_SMOKE_ALARM, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ] BATTERY_POWER_SENSOR = [ @@ -131,6 +135,7 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ATTR_DEVICE_SOIL_TH_SENSOR, ATTR_DEVICE_SMOKE_ALARM, + ATTR_DEVICE_SPRINKLER_V2, ] MCU_DEV_TEMPERATURE_SENSOR = [ diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 0eb9de97469..9e60b77f43a 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -47,6 +47,9 @@ "exceptions": { "invalid_config_entry": { "message": "Config entry not found or not loaded!" + }, + "valve_inoperable_currently": { + "message": "The Valve cannot be operated currently." } }, "entity": { @@ -118,6 +121,19 @@ }, "meter_valve_2_state": { "name": "Valve 2" + }, + "sprinkler_valve": { + "name": "[%key:component::valve::title%]" + } + }, + "select": { + "sprinkler_mode": { + "name": "Mode", + "state": { + "auto": "[%key:common::state::auto%]", + "manual": "[%key:common::state::manual%]", + "off": "[%key:common::state::off%]" + } } } }, diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py index 06dee8af540..8361724a3cf 100644 --- a/homeassistant/components/yolink/valve.py +++ b/homeassistant/components/yolink/valve.py @@ -4,11 +4,14 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from yolink.client_request import ClientRequest from yolink.const import ( ATTR_DEVICE_MODEL_A, ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ATTR_DEVICE_WATER_METER_CONTROLLER, ) from yolink.device import YoLinkDevice @@ -21,6 +24,7 @@ from homeassistant.components.valve import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DEV_MODEL_WATER_METER_YS5007, DOMAIN @@ -35,6 +39,20 @@ class YoLinkValveEntityDescription(ValveEntityDescription): exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True value: Callable = lambda state: state channel_index: int | None = None + should_update_entity: Callable = lambda state: True + is_available: Callable[[YoLinkDevice, dict[str, Any]], bool] = ( + lambda device, state: True + ) + + +def sprinkler_valve_available(device: YoLinkDevice, data: dict[str, Any]) -> bool: + """Check if sprinkler valve is available.""" + if device.device_type == ATTR_DEVICE_SPRINKLER_V2: + return True + if (state := data.get("state")) is not None: + if (mode := state.get("mode")) is not None: + return mode == "manual" + return False DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( @@ -67,11 +85,24 @@ DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( ), channel_index=1, ), + YoLinkValveEntityDescription( + key="valve", + translation_key="sprinkler_valve", + device_class=ValveDeviceClass.WATER, + value=lambda value: value is False if value is not None else None, + exists_fn=lambda device: ( + device.device_type in [ATTR_DEVICE_SPRINKLER, ATTR_DEVICE_SPRINKLER_V2] + ), + should_update_entity=lambda value: value is not None, + is_available=sprinkler_valve_available, + ), ) DEVICE_TYPE = [ ATTR_DEVICE_WATER_METER_CONTROLLER, ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ] @@ -123,13 +154,24 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity): attr_val := self.entity_description.value( state.get(self.entity_description.key) ) - ) is None: + ) is None and self.entity_description.should_update_entity(attr_val) is False: return - self._attr_is_closed = attr_val + if self.entity_description.is_available(self.coordinator.device, state) is True: + self._attr_is_closed = attr_val + self._attr_available = True + else: + self._attr_available = False self.async_write_ha_state() async def _async_invoke_device(self, state: str) -> None: """Call setState api to change valve state.""" + if ( + self.coordinator.device.is_support_mode_switching() + and self.coordinator.dev_net_type == ATTR_DEVICE_MODEL_A + ): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="valve_inoperable_currently" + ) if ( self.coordinator.device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER @@ -139,6 +181,16 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity): await self.call_device( ClientRequest("setState", {"valves": {str(channel_index): state}}) ) + if self.coordinator.device.device_type == ATTR_DEVICE_SPRINKLER: + await self.call_device( + ClientRequest( + "setManualWater", {"state": "start" if state == "open" else "stop"} + ) + ) + if self.coordinator.device.device_type == ATTR_DEVICE_SPRINKLER_V2: + await self.call_device( + ClientRequest("setState", {"running": state == "open"}) + ) else: await self.call_device(ClientRequest("setState", {"valve": state})) self._attr_is_closed = state == "close" @@ -155,10 +207,4 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity): @property def available(self) -> bool: """Return true is device is available.""" - if ( - self.coordinator.device.is_support_mode_switching() - and self.coordinator.dev_net_type is not None - ): - # When the device operates in Class A mode, it cannot be controlled. - return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A - return super().available + return self._attr_available and super().available diff --git a/homeassistant/components/zeversolar/entity.py b/homeassistant/components/zeversolar/entity.py index 18ac4dcde32..3e085d952ca 100644 --- a/homeassistant/components/zeversolar/entity.py +++ b/homeassistant/components/zeversolar/entity.py @@ -27,4 +27,5 @@ class ZeversolarEntity( identifiers={(DOMAIN, coordinator.data.serial_number)}, name="Zeversolar Sensor", manufacturer="Zeversolar", + serial_number=coordinator.data.serial_number, ) diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 6c5fcba1f8b..4383aa52afa 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -8,6 +8,7 @@ from typing import Any from zha.application.const import ATTR_IEEE from zha.application.gateway import Gateway +from zigpy.application import ControllerApplication from zigpy.config import CONF_NWK_EXTENDED_PAN_ID from zigpy.types import Channels @@ -63,6 +64,19 @@ def shallow_asdict(obj: Any) -> dict: return obj +def get_application_state_diagnostics(app: ControllerApplication) -> dict: + """Dump the application state as a dictionary.""" + data = shallow_asdict(app.state) + + # EUI64 objects in zigpy are not subclasses of any JSON-serializable key type and + # must be converted to strings. + data["network_info"]["nwk_addresses"] = { + str(k): v for k, v in data["network_info"]["nwk_addresses"].items() + } + + return data + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: @@ -79,7 +93,7 @@ async def async_get_config_entry_diagnostics( { "config": zha_data.yaml_config, "config_entry": config_entry.as_dict(), - "application_state": shallow_asdict(app.state), + "application_state": get_application_state_diagnostics(app), "energy_scan": { channel: 100 * energy / 255 for channel, energy in energy_scan.items() }, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9842fa7a0f3..9f5e6a91905 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.67"], + "requirements": ["zha==0.0.69"], "usb": [ { "vid": "10C4", @@ -106,6 +106,12 @@ "pid": "EA60", "description": "*sonoff*max*", "known_devices": ["SONOFF Dongle Max MG24"] + }, + { + "vid": "10C4", + "pid": "EA60", + "description": "*sonoff*lite*mg21*", + "known_devices": ["sonoff zigbee dongle lite mg21"] } ], "zeroconf": [ diff --git a/homeassistant/components/zimi/manifest.json b/homeassistant/components/zimi/manifest.json index 3e019d2f053..58a56c97830 100644 --- a/homeassistant/components/zimi/manifest.json +++ b/homeassistant/components/zimi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/zimi", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["zcc-helper==3.5.2"] + "requirements": ["zcc-helper==3.6"] } diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 6121bd00508..92912a2cdb5 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -35,6 +35,7 @@ from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import selector from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.usb import UsbServiceInfo @@ -88,16 +89,36 @@ ADDON_USER_INPUT_MAP = { CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY, } +CONF_ADDON_RF_REGION = "rf_region" + +EXAMPLE_SERVER_URL = "ws://localhost:3000" ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61") NETWORK_TYPE_NEW = "new" NETWORK_TYPE_EXISTING = "existing" +ZWAVE_JS_SERVER_INSTRUCTIONS = ( + "https://www.home-assistant.io/integrations/zwave_js/" + "#advanced-installation-instructions" +) ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = ( "https://www.home-assistant.io/integrations/zwave_js/" "#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui" ) +RF_REGIONS = [ + "Australia/New Zealand", + "China", + "Europe", + "Hong Kong", + "India", + "Israel", + "Japan", + "Korea", + "Russia", + "USA", +] + def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: """Return a schema for the manual step.""" @@ -190,10 +211,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self.backup_data: bytes | None = None self.backup_filepath: Path | None = None self.use_addon = False + self._addon_config_updates: dict[str, Any] = {} self._migrating = False self._reconfigure_config_entry: ZwaveJSConfigEntry | None = None self._usb_discovery = False self._recommended_install = False + self._rf_region: str | None = None async def async_step_install_addon( self, user_input: dict[str, Any] | None = None @@ -231,6 +254,21 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Start Z-Wave JS add-on.""" + if self.hass.config.country is None and ( + not self._rf_region or self._rf_region == "Automatic" + ): + # If the country is not set, we need to check the RF region add-on config. + addon_info = await self._async_get_addon_info() + rf_region: str | None = addon_info.options.get(CONF_ADDON_RF_REGION) + self._rf_region = rf_region + if rf_region is None or rf_region == "Automatic": + # If the RF region is not set, we need to ask the user to select it. + return await self.async_step_rf_region() + if config_updates := self._addon_config_updates: + # If we have updates to the add-on config, set them before starting the add-on. + self._addon_config_updates = {} + await self._async_set_addon_config(config_updates) + if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -529,7 +567,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a manual configuration.""" if user_input is None: return self.async_show_form( - step_id="manual", data_schema=get_manual_schema({}) + step_id="manual", + data_schema=get_manual_schema({}), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, ) errors = {} @@ -558,7 +601,13 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return self._async_create_entry_from_vars() return self.async_show_form( - step_id="manual", data_schema=get_manual_schema(user_input), errors=errors + step_id="manual", + data_schema=get_manual_schema(user_input), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, + errors=errors, ) async def async_step_hassio( @@ -613,6 +662,33 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_on_supervisor({CONF_USE_ADDON: True}) return await self.async_step_on_supervisor() + async def async_step_rf_region( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle RF region selection step.""" + if user_input is not None: + # Store the selected RF region + self._addon_config_updates[CONF_ADDON_RF_REGION] = self._rf_region = ( + user_input["rf_region"] + ) + return await self.async_step_start_addon() + + schema = vol.Schema( + { + vol.Required("rf_region"): selector.SelectSelector( + selector.SelectSelectorConfig( + options=RF_REGIONS, + mode=selector.SelectSelectorMode.DROPDOWN, + ) + ), + } + ) + + return self.async_show_form( + step_id="rf_region", + data_schema=schema, + ) + async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -712,7 +788,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } - await self._async_set_addon_config(addon_config_updates) + self._addon_config_updates = addon_config_updates return await self.async_step_start_addon() # Network already exists, go to security keys step @@ -783,7 +859,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } - await self._async_set_addon_config(addon_config_updates) + self._addon_config_updates = addon_config_updates return await self.async_step_start_addon() data_schema = vol.Schema( @@ -988,7 +1064,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: if self.usb_path: # USB discovery was used, so the device is already known. - await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) + self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path return await self.async_step_start_addon() # Now that the old controller is gone, we can scan for serial ports again return await self.async_step_choose_serial_port() @@ -1016,6 +1092,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="manual_reconfigure", data_schema=get_manual_schema({CONF_URL: config_entry.data[CONF_URL]}), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, ) errors = {} @@ -1046,6 +1126,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="manual_reconfigure", data_schema=get_manual_schema(user_input), + description_placeholders={ + "example_server_url": EXAMPLE_SERVER_URL, + "server_instructions": ZWAVE_JS_SERVER_INSTRUCTIONS, + }, errors=errors, ) @@ -1112,6 +1196,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } + addon_config_updates = self._addon_config_updates | addon_config_updates + self._addon_config_updates = {} await self._async_set_addon_config(addon_config_updates) if addon_info.state == AddonState.RUNNING and not self.restart_addon: @@ -1183,7 +1269,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): """Choose a serial port.""" if user_input is not None: self.usb_path = user_input[CONF_USB_PATH] - await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) + self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path return await self.async_step_start_addon() try: diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 25c342cf87d..7030009f5ad 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -760,7 +760,7 @@ DISCOVERY_SCHEMAS = [ platform=Platform.SELECT, hint="multilevel_switch", manufacturer_id={0x0084}, - product_id={0x0107, 0x0108, 0x010B, 0x0205}, + product_id={0x0107, 0x0108, 0x0109, 0x010B, 0x0205}, product_type={0x0311, 0x0313, 0x0331, 0x0341, 0x0343}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, data_template=BaseDiscoverySchemaDataTemplate( diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 2efb8c8e67c..23b906a9d16 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -4,15 +4,15 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import Any +from typing import Any, cast import voluptuous as vol -from zwave_js_server.const import CommandClass +from zwave_js_server.const import CommandClass, RssiError from zwave_js_server.const.command_class.meter import ( RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TYPE, ) -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, RssiErrorReceived from zwave_js_server.model.controller import Controller from zwave_js_server.model.controller.statistics import ControllerStatistics from zwave_js_server.model.driver import Driver @@ -1049,7 +1049,7 @@ class ZWaveStatisticsSensor(SensorEntity): self, config_entry: ZwaveJSConfigEntry, driver: Driver, - statistics_src: ZwaveNode | Controller, + statistics_src: Controller | ZwaveNode, description: ZWaveJSStatisticsSensorEntityDescription, ) -> None: """Initialize a Z-Wave statistics entity.""" @@ -1080,13 +1080,31 @@ class ZWaveStatisticsSensor(SensorEntity): ) @callback - def statistics_updated(self, event_data: dict) -> None: + def _statistics_updated(self, event_data: dict) -> None: """Call when statistics updated event is received.""" - self._attr_native_value = self.entity_description.convert( - event_data["statistics_updated"], self.entity_description.key + statistics = cast( + ControllerStatistics | NodeStatistics, event_data["statistics_updated"] ) + self._set_statistics(statistics) self.async_write_ha_state() + @callback + def _set_statistics( + self, statistics: ControllerStatistics | NodeStatistics + ) -> None: + """Set updated statistics.""" + try: + self._attr_native_value = self.entity_description.convert( + statistics, self.entity_description.key + ) + except RssiErrorReceived as err: + if err.error is RssiError.NOT_AVAILABLE: + self._attr_available = False + return + self._attr_native_value = None + # Reset available state. + self._attr_available = True + async def async_added_to_hass(self) -> None: """Call when entity is added.""" self.async_on_remove( @@ -1104,10 +1122,8 @@ class ZWaveStatisticsSensor(SensorEntity): ) ) self.async_on_remove( - self.statistics_src.on("statistics updated", self.statistics_updated) + self.statistics_src.on("statistics updated", self._statistics_updated) ) # Set initial state - self._attr_native_value = self.entity_description.convert( - self.statistics_src.statistics, self.entity_description.key - ) + self._set_statistics(self.statistics_src.statistics) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 8ac356a40b0..fffcb2ca9dd 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -82,13 +82,21 @@ "title": "Installing add-on" }, "manual": { + "description": "The Z-Wave integration requires a running Z-Wave Server. If you don't already have that set up, please read the [instructions]({server_instructions}) in our documentation.\n\nWhen you have a Z-Wave Server running, enter its URL below to allow the integration to connect.", "data": { "url": "[%key:common::config_flow::data::url%]" + }, + "data_description": { + "url": "The URL of the Z-Wave Server WebSocket API, e.g. {example_server_url}" } }, "manual_reconfigure": { + "description": "[%key:component::zwave_js::config::step::manual::description%]", "data": { "url": "[%key:common::config_flow::data::url%]" + }, + "data_description": { + "url": "[%key:component::zwave_js::config::step::manual::data_description::url%]" } }, "on_supervisor": { @@ -105,6 +113,16 @@ "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", "title": "[%key:component::zwave_js::config::step::on_supervisor::title%]" }, + "rf_region": { + "title": "Z-Wave region", + "description": "Select the RF region for your Z-Wave network.", + "data": { + "rf_region": "RF region" + }, + "data_description": { + "rf_region": "The radio frequency region for your Z-Wave network. This must match the region of your Z-Wave devices." + } + }, "start_addon": { "title": "Configuring add-on" }, diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 88e1a22c00f..9e9d6ee2ef3 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -42,7 +42,7 @@ from .models import ZwaveJSConfigEntry PARALLEL_UPDATES = 1 UPDATE_DELAY_STRING = "delay" -UPDATE_DELAY_INTERVAL = 5 # In minutes +UPDATE_DELAY_INTERVAL = 15 # In seconds ATTR_LATEST_VERSION_FIRMWARE = "latest_version_firmware" @@ -129,11 +129,11 @@ async def async_setup_entry( @callback def async_add_firmware_update_entity(node: ZwaveNode) -> None: """Add firmware update entity.""" - # We need to delay the first update of each entity to avoid flooding the network - # so we maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL - # minute increments. + # Delay the first update of each entity to avoid spamming the firmware server. + # Maintain a counter to schedule first update in UPDATE_DELAY_INTERVAL + # second increments. cnt[UPDATE_DELAY_STRING] += 1 - delay = timedelta(minutes=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL)) + delay = timedelta(seconds=(cnt[UPDATE_DELAY_STRING] * UPDATE_DELAY_INTERVAL)) driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. if node.is_controller_node: @@ -413,7 +413,8 @@ class ZWaveFirmwareUpdateEntity(UpdateEntity): ): self._attr_latest_version = self._attr_installed_version - # Spread updates out in 5 minute increments to avoid flooding the network + # Spread updates out in 15 second increments + # to avoid spamming the firmware server self.async_on_remove( async_call_later(self.hass, self._delay, self._async_update) ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index da8e73d9566..f5ccf9c3143 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3385,6 +3385,34 @@ class ConfigSubentryFlow( return result + @callback + def _async_update( + self, + entry: ConfigEntry, + subentry: ConfigSubentry, + *, + unique_id: str | None | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, + data: Mapping[str, Any] | UndefinedType = UNDEFINED, + data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED, + ) -> bool: + """Update config subentry and return result. + + Internal to be used by update_and_abort and update_reload_and_abort methods only. + """ + + if data_updates is not UNDEFINED: + if data is not UNDEFINED: + raise ValueError("Cannot set both data and data_updates") + data = subentry.data | data_updates + return self.hass.config_entries.async_update_subentry( + entry=entry, + subentry=subentry, + unique_id=unique_id, + title=title, + data=data, + ) + @callback def async_update_and_abort( self, @@ -3404,19 +3432,52 @@ class ConfigSubentryFlow( :param title: replace the title of the subentry :param unique_id: replace the unique_id of the subentry """ - if data_updates is not UNDEFINED: - if data is not UNDEFINED: - raise ValueError("Cannot set both data and data_updates") - data = subentry.data | data_updates - self.hass.config_entries.async_update_subentry( + self._async_update( entry=entry, subentry=subentry, unique_id=unique_id, title=title, data=data, + data_updates=data_updates, ) return self.async_abort(reason="reconfigure_successful") + @callback + def async_update_reload_and_abort( + self, + entry: ConfigEntry, + subentry: ConfigSubentry, + *, + unique_id: str | None | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, + data: Mapping[str, Any] | UndefinedType = UNDEFINED, + data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED, + reload_even_if_entry_is_unchanged: bool = True, + ) -> SubentryFlowResult: + """Update config subentry, reload config entry and finish subentry flow. + + :param data: replace the subentry data with new data + :param data_updates: add items from data_updates to subentry data - existing + keys are overridden + :param title: replace the title of the subentry + :param unique_id: replace the unique_id of the subentry + :param reload_even_if_entry_is_unchanged: set this to `False` if the entry + should not be reloaded if it is unchanged + """ + result = self._async_update( + entry=entry, + subentry=subentry, + unique_id=unique_id, + title=title, + data=data, + data_updates=data_updates, + ) + if reload_even_if_entry_is_unchanged or result: + if entry.update_listeners: + raise ValueError("Cannot update and reload entry with update listeners") + self.hass.config_entries.async_schedule_reload(entry.entry_id) + return self.async_abort(reason="reconfigure_successful") + @property def _entry_id(self) -> str: """Return config entry id.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index b678e02569c..16d361a7957 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -588,6 +588,7 @@ ATTR_PERSONS: Final = "persons" class UnitOfApparentPower(StrEnum): """Apparent power units.""" + MILLIVOLT_AMPERE = "mVA" VOLT_AMPERE = "VA" @@ -608,6 +609,7 @@ class UnitOfPower(StrEnum): class UnitOfReactivePower(StrEnum): """Reactive power units.""" + MILLIVOLT_AMPERE_REACTIVE = "mvar" VOLT_AMPERE_REACTIVE = "var" KILO_VOLT_AMPERE_REACTIVE = "kvar" @@ -669,7 +671,7 @@ class UnitOfElectricCurrent(StrEnum): class UnitOfElectricPotential(StrEnum): """Electric potential units.""" - MICROVOLT = "µV" + MICROVOLT = "μV" MILLIVOLT = "mV" VOLT = "V" KILOVOLT = "kV" @@ -781,6 +783,7 @@ class UnitOfVolumeFlowRate(StrEnum): """Volume flow rate units.""" CUBIC_METERS_PER_HOUR = "m³/h" + CUBIC_METERS_PER_MINUTE = "m³/min" CUBIC_METERS_PER_SECOND = "m³/s" CUBIC_FEET_PER_MINUTE = "ft³/min" LITERS_PER_HOUR = "L/h" @@ -819,7 +822,7 @@ class UnitOfMass(StrEnum): GRAMS = "g" KILOGRAMS = "kg" MILLIGRAMS = "mg" - MICROGRAMS = "µg" + MICROGRAMS = "μg" OUNCES = "oz" POUNDS = "lb" STONES = "st" @@ -837,13 +840,13 @@ class UnitOfConductivity( """Conductivity units.""" SIEMENS_PER_CM = "S/cm" - MICROSIEMENS_PER_CM = "µS/cm" + MICROSIEMENS_PER_CM = "μS/cm" MILLISIEMENS_PER_CM = "mS/cm" # Deprecated aliases SIEMENS = "S/cm" """Deprecated: Please use UnitOfConductivity.SIEMENS_PER_CM""" - MICROSIEMENS = "µS/cm" + MICROSIEMENS = "μS/cm" """Deprecated: Please use UnitOfConductivity.MICROSIEMENS_PER_CM""" MILLISIEMENS = "mS/cm" """Deprecated: Please use UnitOfConductivity.MILLISIEMENS_PER_CM""" @@ -915,8 +918,8 @@ class UnitOfPrecipitationDepth(StrEnum): # Concentration units CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³" -CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" +CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "μg/m³" CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³" CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 0abd4365feb..f3b83e39df9 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -4,6 +4,7 @@ To update, run python3 -m script.hassfest """ APPLICATION_CREDENTIALS = [ + "august", "electric_kiwi", "fitbit", "geocaching", diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index da6cab4bc22..fcaa824ff39 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -834,6 +834,12 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ ], "manufacturer_id": 76, }, + { + "connectable": True, + "domain": "togrill", + "manufacturer_id": 34714, + "service_uuid": "0000cee0-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "xiaomi_ble", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5816a0ddbd9..19fb5491465 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -348,7 +348,6 @@ FLOWS = { "lg_thinq", "lidarr", "lifx", - "linear_garage_door", "linkplay", "litejet", "litterrobot", @@ -577,6 +576,7 @@ FLOWS = { "sky_remote", "skybell", "slack", + "sleep_as_android", "sleepiq", "slide_local", "slimproto", @@ -654,6 +654,7 @@ FLOWS = { "tilt_pi", "time_date", "todoist", + "togrill", "tolo", "tomorrowio", "toon", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c606d79f2c5..10f5ea45427 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3512,12 +3512,6 @@ "integration_type": "virtual", "supported_by": "idasen_desk" }, - "linear_garage_door": { - "name": "Linear Garage Door", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "linkedgo": { "name": "LinkedGo", "integration_type": "virtual", @@ -3839,11 +3833,6 @@ "config_flow": false, "iot_class": "cloud_polling" }, - "mercury_nz": { - "name": "Mercury NZ Limited", - "integration_type": "virtual", - "supported_by": "opower" - }, "message_bird": { "name": "MessageBird", "integration_type": "hub", @@ -6005,6 +5994,12 @@ "config_flow": true, "iot_class": "cloud_push" }, + "sleep_as_android": { + "name": "Sleep as Android", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "sleepiq": { "name": "SleepIQ", "integration_type": "hub", @@ -6796,6 +6791,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "togrill": { + "name": "ToGrill", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "tolo": { "name": "TOLO Sauna", "integration_type": "hub", diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 18623926ce2..dee0367de24 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -143,6 +143,12 @@ USB = [ "pid": "EA60", "vid": "10C4", }, + { + "description": "*sonoff*lite*mg21*", + "domain": "zha", + "pid": "EA60", + "vid": "10C4", + }, { "domain": "zwave_js", "pid": "0200", diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 1671e8e2cc2..0f8bdfd7793 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -155,7 +155,7 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): @property def name(self) -> str: """Name of the implementation.""" - return "Configuration.yaml" + return "Local application credentials" @property def domain(self) -> str: diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 22074fb90a7..9ace020f342 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -31,27 +31,27 @@ class _BaseFlowManagerView(HomeAssistantView, Generic[_FlowManagerT]): def _prepare_result_json( self, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" + ) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - data = result.copy() assert "result" not in result - data.pop("data") - data.pop("context") - return data + return { + key: val + for key, val in result.items() + if key not in ("data", "context") + } + + data = dict(result) if "data_schema" not in result: - return result + return data - data = result.copy() - - if (schema := data["data_schema"]) is None: - data["data_schema"] = [] # type: ignore[typeddict-item] # json result type + if (schema := result["data_schema"]) is None: + data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert( schema, custom_serializer=cv.custom_serializer ) - return data diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c7f7d4c369d..463b5c4dddc 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1460,12 +1460,18 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if config_entry_id not in config_entries: continue if config_entries == {config_entry_id}: + # Clear disabled_by if it was disabled by the config entry + if deleted_device.disabled_by is DeviceEntryDisabler.CONFIG_ENTRY: + disabled_by = None + else: + disabled_by = deleted_device.disabled_by # Add a time stamp when the deleted device became orphaned self.deleted_devices[deleted_device.id] = attr.evolve( deleted_device, orphaned_timestamp=now_time, config_entries=set(), config_entries_subentries={}, + disabled_by=disabled_by, ) else: config_entries = config_entries - {config_entry_id} diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index d972b421fc4..2125c0f4512 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1613,9 +1613,17 @@ class EntityRegistry(BaseRegistry): for key, deleted_entity in list(self.deleted_entities.items()): if config_entry_id != deleted_entity.config_entry_id: continue + # Clear disabled_by if it was disabled by the config entry + if deleted_entity.disabled_by is RegistryEntryDisabler.CONFIG_ENTRY: + disabled_by = None + else: + disabled_by = deleted_entity.disabled_by # Add a time stamp when the deleted entity became orphaned self.deleted_entities[key] = attr.evolve( - deleted_entity, orphaned_timestamp=now_time, config_entry_id=None + deleted_entity, + orphaned_timestamp=now_time, + config_entry_id=None, + disabled_by=disabled_by, ) self.async_schedule_save() diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 176bcfcd7c4..8af91249200 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -12,39 +12,7 @@ from typing import TYPE_CHECKING, Any, Final import orjson from homeassistant.util.file import write_utf8_file, write_utf8_file_atomic -from homeassistant.util.json import ( - JSON_DECODE_EXCEPTIONS as _JSON_DECODE_EXCEPTIONS, - JSON_ENCODE_EXCEPTIONS as _JSON_ENCODE_EXCEPTIONS, - SerializationError, - format_unserializable_data, - json_loads as _json_loads, -) - -from .deprecation import ( - DeprecatedConstant, - all_with_deprecated_constants, - check_if_deprecated_constant, - deprecated_function, - dir_with_deprecated_constants, -) - -_DEPRECATED_JSON_DECODE_EXCEPTIONS = DeprecatedConstant( - _JSON_DECODE_EXCEPTIONS, "homeassistant.util.json.JSON_DECODE_EXCEPTIONS", "2025.8" -) -_DEPRECATED_JSON_ENCODE_EXCEPTIONS = DeprecatedConstant( - _JSON_ENCODE_EXCEPTIONS, "homeassistant.util.json.JSON_ENCODE_EXCEPTIONS", "2025.8" -) -json_loads = deprecated_function( - "homeassistant.util.json.json_loads", breaks_in_ha_version="2025.8" -)(_json_loads) - -# These can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial( - dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] -) -__all__ = all_with_deprecated_constants(globals()) - +from homeassistant.util.json import SerializationError, format_unserializable_data _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 1ff6b188214..dc69916a728 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -183,6 +183,7 @@ class ToolInput: tool_args: dict[str, Any] # Using lambda for default to allow patching in tests id: str = dc_field(default_factory=lambda: ulid_now()) # pylint: disable=unnecessary-lambda + external: bool = False class Tool: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index ad0c909003e..1003991ccec 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1333,6 +1333,7 @@ class StateSelectorConfig(BaseSelectorConfig, total=False): entity_id: str hide_states: list[str] + multiple: bool @SELECTORS.register("state") @@ -1350,6 +1351,7 @@ class StateSelector(Selector[StateSelectorConfig]): # selectors into two types: one for state and one for attribute. # Limiting the public use, prevents breaking changes in the future. # vol.Optional("attribute"): str, + vol.Optional("multiple", default=False): cv.boolean, } ) @@ -1357,10 +1359,14 @@ class StateSelector(Selector[StateSelectorConfig]): """Instantiate a selector.""" super().__init__(config) - def __call__(self, data: Any) -> str: + def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" - state: str = vol.Schema(str)(data) - return state + if not self.config["multiple"]: + state: str = vol.Schema(str)(data) + return state + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] class StatisticSelectorConfig(BaseSelectorConfig, total=False): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 28e7491c48c..387c1ada21b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,9 +1,9 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==1.2.0 +aiodhcpwatcher==1.2.1 aiodiscover==2.7.1 aiodns==3.5.0 -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 aiohttp==3.12.15 @@ -20,7 +20,7 @@ audioop-lts==0.2.1 av==13.1.0 awesomeversion==25.5.0 bcrypt==4.3.0 -bleak-retry-connector==4.0.0 +bleak-retry-connector==4.3.0 bleak==1.0.1 bluetooth-adapters==2.0.0 bluetooth-auto-recovery==1.5.2 @@ -34,18 +34,18 @@ dbus-fast==2.44.3 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==4.0.2 -hass-nabucasa==0.111.1 -hassil==2.2.3 +habluetooth==5.1.0 +hass-nabucasa==1.0.0 +hassil==3.1.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250806.0 +home-assistant-frontend==20250811.1 home-assistant-intents==2025.7.30 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.11.1 +orjson==3.11.2 packaging>=23.1 paho-mqtt==2.1.0 Pillow==11.3.0 @@ -65,12 +65,12 @@ securetar==2025.2.1 SQLAlchemy==2.0.41 standard-aifc==3.13.0 standard-telnetlib==3.13.0 -typing-extensions>=4.14.0,<5.0 +typing-extensions>=4.15.0,<5.0 ulid-transform==1.4.0 urllib3>=2.0 -uv==0.7.1 +uv==0.8.9 voluptuous-openapi==0.1.0 -voluptuous-serialize==2.6.0 +voluptuous-serialize==2.7.0 voluptuous==0.15.2 webrtc-models==0.3.0 yarl==1.20.1 @@ -144,7 +144,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==6.31.1 +protobuf==6.32.0 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder @@ -168,7 +168,7 @@ poetry==1000000000.0.0 # We want to skip the binary wheels for the 'charset-normalizer' packages. # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. -charset-normalizer==3.4.0 +charset-normalizer==3.4.3 # dacite: Ensure we have a version that is able to handle type unions for # NAM, Brother, and GIOS. @@ -212,3 +212,14 @@ multidict>=6.4.2 # rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 rpds-py==0.26.0 + +# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI +num2words==0.5.14 + +# pymodbus does not follow SemVer, and it keeps getting +# downgraded or upgraded by custom components +# This ensures all use the same version +pymodbus==3.11.1 + +# Some packages don't support gql 4.0.0 yet +gql<4.0.0 diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 5bde108dfc1..4d6d2365617 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, UNIT_NOT_RECOGNIZED_TEMPLATE, + UnitOfApparentPower, UnitOfArea, UnitOfBloodGlucoseConcentration, UnitOfConductivity, @@ -28,6 +29,7 @@ from homeassistant.const import ( UnitOfPower, UnitOfPressure, UnitOfReactiveEnergy, + UnitOfReactivePower, UnitOfSpeed, UnitOfTemperature, UnitOfTime, @@ -382,6 +384,20 @@ class MassConverter(BaseUnitConverter): } +class ApparentPowerConverter(BaseUnitConverter): + """Utility to convert apparent power values.""" + + UNIT_CLASS = "apparent_power" + _UNIT_CONVERSION: dict[str | None, float] = { + UnitOfApparentPower.MILLIVOLT_AMPERE: 1 * 1000, + UnitOfApparentPower.VOLT_AMPERE: 1, + } + VALID_UNITS = { + UnitOfApparentPower.MILLIVOLT_AMPERE, + UnitOfApparentPower.VOLT_AMPERE, + } + + class PowerConverter(BaseUnitConverter): """Utility to convert power values.""" @@ -445,6 +461,22 @@ class ReactiveEnergyConverter(BaseUnitConverter): VALID_UNITS = set(UnitOfReactiveEnergy) +class ReactivePowerConverter(BaseUnitConverter): + """Utility to convert reactive power values.""" + + UNIT_CLASS = "reactive_power" + _UNIT_CONVERSION: dict[str | None, float] = { + UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE: 1 * 1000, + UnitOfReactivePower.VOLT_AMPERE_REACTIVE: 1, + UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE: 1 / 1000, + } + VALID_UNITS = { + UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE, + UnitOfReactivePower.VOLT_AMPERE_REACTIVE, + UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE, + } + + class SpeedConverter(BaseUnitConverter): """Utility to convert speed values.""" @@ -737,6 +769,7 @@ class VolumeFlowRateConverter(BaseUnitConverter): # Units in terms of m³/h _UNIT_CONVERSION: dict[str | None, float] = { UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: 1, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_MINUTE: 1 / _HRS_TO_MINUTES, UnitOfVolumeFlowRate.CUBIC_METERS_PER_SECOND: 1 / _HRS_TO_SECS, UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: 1 / (_HRS_TO_MINUTES * _CUBIC_FOOT_TO_CUBIC_METER), @@ -752,6 +785,7 @@ class VolumeFlowRateConverter(BaseUnitConverter): VALID_UNITS = { UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_MINUTE, UnitOfVolumeFlowRate.CUBIC_METERS_PER_SECOND, UnitOfVolumeFlowRate.LITERS_PER_HOUR, UnitOfVolumeFlowRate.LITERS_PER_MINUTE, diff --git a/mypy.ini b/mypy.ini index 8482138cc45..ad9196c80c5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2856,16 +2856,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.linear_garage_door.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.linkplay.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -4426,6 +4416,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.sleep_as_android.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.sleepiq.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pylint/plugins/hass_enforce_greek_micro_char.py b/pylint/plugins/hass_enforce_greek_micro_char.py new file mode 100644 index 00000000000..909af66cd9e --- /dev/null +++ b/pylint/plugins/hass_enforce_greek_micro_char.py @@ -0,0 +1,76 @@ +"""Plugin for checking preferred coding of μ is used.""" + +from __future__ import annotations + +from typing import Any + +from astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + + +class HassEnforceGreekMicroCharChecker(BaseChecker): + """Checker for micro char.""" + + name = "hass-enforce-greek-micro-char" + priority = -1 + msgs = { + "W7452": ( + "Constants with a micro unit prefix must encode the " + "small Greek Letter Mu as U+03BC (\u03bc), not as U+00B5 (\u00b5)", + "hass-enforce-greek-micro-char", + "According to [The Unicode Consortium]" + "(https://en.wikipedia.org/wiki/Micro-#Symbol_encoding_in_character_sets)," + " the Greek letter character is preferred. " + "To search a specific encoded μ char in Microsoft Visual Studio Code, " + 'make sure the "Match case" option is enabled. Note that this only works ' + "when searching globally, and not while searching a single document.", + ), + } + options = () + + def visit_annassign(self, node: nodes.AnnAssign) -> None: + """Check for micro char const or StrEnum with type annotations.""" + self._do_micro_check(node.target, node) + + def visit_assign(self, node: nodes.Assign) -> None: + """Check for micro char const without type annotations.""" + for target in node.targets: + self._do_micro_check(target, node) + + def _do_micro_check( + self, target: nodes.NodeNG, node: nodes.Assign | nodes.AnnAssign + ) -> None: + """Check const assignment is not containing ANSI micro char.""" + + def _check_const(node_const: nodes.Const | Any) -> bool: + if ( + isinstance(node_const, nodes.Const) + and isinstance(node_const.value, str) + and "\u00b5" in node_const.value + ): + self.add_message(self.name, node=node) + return True + return False + + # Check constant assignments + if ( + isinstance(target, nodes.AssignName) + and isinstance(node.value, nodes.Const) + and _check_const(node.value) + ): + return + + # Check dict with EntityDescription calls + if isinstance(target, nodes.AssignName) and isinstance(node.value, nodes.Dict): + for _, subnode in node.value.items: + if not isinstance(subnode, nodes.Call): + continue + for keyword in subnode.keywords: + if _check_const(keyword.value): + return + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassEnforceGreekMicroCharChecker(linter)) diff --git a/pyproject.toml b/pyproject.toml index 0125d5b1bbc..98586e97595 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 - "aiohasupervisor==0.3.1", + "aiohasupervisor==0.3.2b0", "aiohttp==3.12.15", "aiohttp_cors==0.8.1", "aiohttp-fast-zlib==0.3.0", @@ -47,7 +47,7 @@ dependencies = [ "fnv-hash-fast==1.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.111.1", + "hass-nabucasa==1.0.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.28.1", @@ -61,7 +61,7 @@ dependencies = [ "Pillow==11.3.0", "propcache==0.3.2", "pyOpenSSL==25.1.0", - "orjson==3.11.1", + "orjson==3.11.2", "packaging>=23.1", "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", @@ -71,12 +71,12 @@ dependencies = [ "SQLAlchemy==2.0.41", "standard-aifc==3.13.0", "standard-telnetlib==3.13.0", - "typing-extensions>=4.14.0,<5.0", + "typing-extensions>=4.15.0,<5.0", "ulid-transform==1.4.0", "urllib3>=2.0", - "uv==0.7.1", + "uv==0.8.9", "voluptuous==0.15.2", - "voluptuous-serialize==2.6.0", + "voluptuous-serialize==2.7.0", "voluptuous-openapi==0.1.0", "yarl==1.20.1", "webrtc-models==0.3.0", @@ -121,6 +121,7 @@ load-plugins = [ "hass_async_load_fixtures", "hass_decorator", "hass_enforce_class_module", + "hass_enforce_greek_micro_char", "hass_enforce_sorted_platforms", "hass_enforce_super_call", "hass_enforce_type_hints", diff --git a/requirements.txt b/requirements.txt index af9a835e0d9..cd288335aad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.5.0 -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 aiohttp==3.12.15 aiohttp_cors==0.8.1 aiohttp-fast-zlib==0.3.0 @@ -22,7 +22,7 @@ certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 -hass-nabucasa==0.111.1 +hass-nabucasa==1.0.0 httpx==0.28.1 home-assistant-bluetooth==1.13.1 ifaddr==0.2.0 @@ -33,7 +33,7 @@ cryptography==45.0.3 Pillow==11.3.0 propcache==0.3.2 pyOpenSSL==25.1.0 -orjson==3.11.1 +orjson==3.11.2 packaging>=23.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 @@ -43,12 +43,12 @@ securetar==2025.2.1 SQLAlchemy==2.0.41 standard-aifc==3.13.0 standard-telnetlib==3.13.0 -typing-extensions>=4.14.0,<5.0 +typing-extensions>=4.15.0,<5.0 ulid-transform==1.4.0 urllib3>=2.0 -uv==0.7.1 +uv==0.8.9 voluptuous==0.15.2 -voluptuous-serialize==2.6.0 +voluptuous-serialize==2.7.0 voluptuous-openapi==0.1.0 yarl==1.20.1 webrtc-models==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3edf7b654ef..0321ee7e8a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ HAP-python==4.9.2 HATasmota==0.10.0 # homeassistant.components.mastodon -Mastodon.py==2.0.1 +Mastodon.py==2.1.2 # homeassistant.components.playstation_network PSNAWP==3.0.0 @@ -48,7 +48,7 @@ ProgettiHWSW==0.1.3 # PyBluez==0.22 # homeassistant.components.cast -PyChromecast==14.0.7 +PyChromecast==14.0.9 # homeassistant.components.flick_electric PyFlick==1.1.3 @@ -84,7 +84,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.68.3 +PySwitchbot==0.69.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 @@ -185,7 +185,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==4.0.0 +aioamazondevices==5.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -207,7 +207,7 @@ aioasuswrt==1.4.0 aioautomower==2.1.2 # homeassistant.components.azure_devops -aioazuredevops==2.2.1 +aioazuredevops==2.2.2 # homeassistant.components.baf aiobafi6==0.9.0 @@ -220,7 +220,7 @@ aiobotocore==2.21.1 aiocomelit==0.12.3 # homeassistant.components.dhcp -aiodhcpwatcher==1.2.0 +aiodhcpwatcher==1.2.1 # homeassistant.components.dhcp aiodiscover==2.7.1 @@ -247,7 +247,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.2.2 +aioesphomeapi==39.0.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -265,7 +265,7 @@ aioguardian==2022.07.0 aioharmony==0.5.2 # homeassistant.components.hassio -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 # homeassistant.components.home_connect aiohomeconnect==0.18.1 @@ -307,7 +307,7 @@ aiolifx==1.2.1 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==2.0.1 +aiolyric==2.0.2 # homeassistant.components.mealie aiomealie==0.10.1 @@ -325,7 +325,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.3 +aiontfy==0.5.4 # homeassistant.components.nut aionut==4.3.4 @@ -375,7 +375,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.0 +aiorussound==4.8.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -417,7 +417,7 @@ aiotedee==0.2.25 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==84 +aiounifi==86 # homeassistant.components.usb aiousbwatcher==1.1.1 @@ -438,7 +438,7 @@ aiowatttime==0.1.1 aiowebdav2==0.4.6 # homeassistant.components.webostv -aiowebostv==0.7.4 +aiowebostv==0.7.5 # homeassistant.components.withings aiowithings==3.1.6 @@ -453,7 +453,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.4 +airos==0.4.3 # homeassistant.components.airthings_ble airthings-ble==0.9.2 @@ -495,7 +495,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.52.0 +anthropic==0.62.0 # homeassistant.components.mcp_server anyio==4.9.0 @@ -527,6 +527,9 @@ arris-tg2492lg==2.2.0 # homeassistant.components.ampio asmog==0.0.6 +# homeassistant.components.asuswrt +asusrouter==1.20.0 + # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms # homeassistant.components.samsungtv @@ -545,7 +548,7 @@ asyncinotify==4.2.0 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.5.3 +asyncsleepiq==1.6.0 # homeassistant.components.aten_pe # atenpdu==0.3.2 @@ -625,7 +628,7 @@ bizkaibus==0.1.1 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.0 +bleak-retry-connector==4.3.0 # homeassistant.components.bluetooth bleak==1.0.1 @@ -640,7 +643,7 @@ blinkpy==0.23.0 blockchain==1.4.4 # homeassistant.components.blue_current -bluecurrent-api==1.2.4 +bluecurrent-api==1.3.1 # homeassistant.components.bluemaestro bluemaestro-ble==0.4.1 @@ -667,7 +670,7 @@ bond-async==0.2.1 bosch-alarm-mode2==0.4.6 # homeassistant.components.bosch_shc -boschshcpy==0.2.91 +boschshcpy==0.2.107 # homeassistant.components.amazon_polly # homeassistant.components.route53 @@ -683,7 +686,7 @@ bring-api==1.1.0 broadlink==0.19.0 # homeassistant.components.brother -brother==5.0.0 +brother==5.0.1 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 @@ -724,9 +727,6 @@ clx-sdk-xms==1.0.0 # homeassistant.components.coinbase coinbase-advanced-py==1.2.2 -# homeassistant.components.coinbase -coinbase==2.1.0 - # homeassistant.scripts.check_config colorlog==6.9.0 @@ -743,7 +743,7 @@ connect-box==0.3.1 construct==2.10.68 # homeassistant.components.cookidoo -cookidoo-api==0.12.2 +cookidoo-api==0.14.0 # homeassistant.components.backup # homeassistant.components.utility_meter @@ -1057,7 +1057,7 @@ google-cloud-speech==2.31.1 google-cloud-texttospeech==2.25.1 # homeassistant.components.google_generative_ai_conversation -google-genai==1.7.0 +google-genai==1.29.0 # homeassistant.components.google_travel_time google-maps-routing==0.6.15 @@ -1127,20 +1127,20 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.1 +habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==4.0.2 +habluetooth==5.1.0 # homeassistant.components.cloud -hass-nabucasa==0.111.1 +hass-nabucasa==1.0.0 # homeassistant.components.splunk hass-splunk==0.1.1 # homeassistant.components.assist_satellite # homeassistant.components.conversation -hassil==2.2.3 +hassil==3.1.0 # homeassistant.components.jewish_calendar hdate[astral]==1.1.2 @@ -1171,10 +1171,10 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.78 +holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250806.0 +home-assistant-frontend==20250811.1 # homeassistant.components.conversation home-assistant-intents==2025.7.30 @@ -1192,7 +1192,7 @@ httplib2==0.20.4 huawei-lte-api==1.11.0 # homeassistant.components.huum -huum==0.8.0 +huum==0.8.1 # homeassistant.components.hyperion hyperion-py==0.7.6 @@ -1240,7 +1240,7 @@ ihcsdk==2.8.5 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.2 +imgw_pib==1.5.4 # homeassistant.components.incomfort incomfort-client==0.6.9 @@ -1307,7 +1307,7 @@ kiwiki-client==0.1.1 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.8.6.52906 +knx-frontend==2025.8.24.205840 # homeassistant.components.konnected konnected==1.2.0 @@ -1325,7 +1325,7 @@ lakeside==0.13 laundrify-aio==1.2.2 # homeassistant.components.lcn -lcn-frontend==0.2.6 +lcn-frontend==0.2.7 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 @@ -1340,10 +1340,10 @@ led-ble==1.1.7 lektricowifi==0.1 # homeassistant.components.letpot -letpot==0.6.1 +letpot==0.6.2 # homeassistant.components.foscam -libpyfoscamcgi==0.0.6 +libpyfoscamcgi==0.0.7 # homeassistant.components.vivotek libpyvivotek==0.4.0 @@ -1363,9 +1363,6 @@ lightwave==0.24 # homeassistant.components.limitlessled limitlessled==1.1.3 -# homeassistant.components.linear_garage_door -linear-garage-door==0.2.9 - # homeassistant.components.linode linode-api==4.1.9b1 @@ -1391,7 +1388,7 @@ lupupy==0.3.2 lw12==0.9.2 # homeassistant.components.scrape -lxml==6.0.0 +lxml==6.0.1 # homeassistant.components.matrix matrix-nio==0.25.2 @@ -1518,7 +1515,7 @@ nextcloudmonitor==1.5.1 nextcord==3.1.0 # homeassistant.components.nextdns -nextdns==4.0.0 +nextdns==4.1.0 # homeassistant.components.niko_home_control nhc==0.4.12 @@ -1594,7 +1591,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.2 +onvif-zeep-async==4.0.4 # homeassistant.components.opengarage open-garage==0.2.0 @@ -1604,7 +1601,7 @@ open-meteo==0.3.2 # homeassistant.components.open_router # homeassistant.components.openai_conversation -openai==1.93.3 +openai==1.99.5 # homeassistant.components.openerz openerz-api==0.3.0 @@ -1628,7 +1625,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.15.1 +opower==0.15.2 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1767,7 +1764,7 @@ py-dormakaba-dkey==1.0.6 py-improv-ble-client==1.0.3 # homeassistant.components.madvr -py-madvr2==1.6.32 +py-madvr2==1.6.40 # homeassistant.components.melissa py-melissa-climate==2.1.4 @@ -1845,14 +1842,11 @@ pyairvisual==2023.08.1 # homeassistant.components.aprilaire pyaprilaire==0.9.1 -# homeassistant.components.asuswrt -pyasuswrt==0.1.21 - # homeassistant.components.atag pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.2.1 +pyatmo==9.2.3 # homeassistant.components.apple_tv pyatv==0.16.1 @@ -1912,7 +1906,7 @@ pycsspeechtts==1.0.8 # pycups==2.0.4 # homeassistant.components.daikin -pydaikin==2.15.0 +pydaikin==2.16.0 # homeassistant.components.danfoss_air pydanfossair==0.1.0 @@ -1963,10 +1957,11 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms +# homeassistant.components.emoncms_history pyemoncms==0.1.2 # homeassistant.components.enphase_envoy -pyenphase==2.2.3 +pyenphase==2.3.0 # homeassistant.components.envisalink pyenvisalink==4.7 @@ -2146,7 +2141,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.2 +pymiele==0.5.4 # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -2155,7 +2150,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.11.0 +pymodbus==3.11.1 # homeassistant.components.monoprice pymonoprice==0.4 @@ -2349,10 +2344,10 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smarla -pysmarlaapi==0.9.1 +pysmarlaapi==0.9.2 # homeassistant.components.smartthings -pysmartthings==3.2.8 +pysmartthings==3.2.9 # homeassistant.components.smarty pysmarty2==0.10.2 @@ -2364,7 +2359,7 @@ pysmhi==1.0.2 pysml==0.1.5 # homeassistant.components.smlight -pysmlight==0.2.7 +pysmlight==0.2.8 # homeassistant.components.snmp pysnmp==7.1.21 @@ -2385,7 +2380,7 @@ pyspeex-noise==1.0.2 pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron -pystiebeleltron==0.1.0 +pystiebeleltron==0.2.3 # homeassistant.components.suez_water pysuezV2==2.0.7 @@ -2466,7 +2461,7 @@ python-linkplay==0.2.12 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==8.0.0 +python-matter-server==8.1.0 # homeassistant.components.melcloud python-melcloud==0.1.0 @@ -2478,7 +2473,7 @@ python-miio==0.5.12 python-mpd2==3.1.1 # homeassistant.components.mystrom -python-mystrom==2.4.0 +python-mystrom==2.5.0 # homeassistant.components.open_router python-open-router==0.3.1 @@ -2512,7 +2507,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.6.6 +python-snoo==0.8.3 # homeassistant.components.songpal python-songpal==0.16.2 @@ -2660,7 +2655,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 @@ -2708,7 +2703,7 @@ rpi-bad-power==0.1.0 russound==0.2.0 # homeassistant.components.ruuvitag_ble -ruuvitag-ble==0.1.2 +ruuvitag-ble==0.2.1 # homeassistant.components.yamaha rxv==0.7.0 @@ -2811,7 +2806,7 @@ soco==0.30.11 solaredge-local==0.2.3 # homeassistant.components.solarlog -solarlog_cli==0.4.0 +solarlog_cli==0.5.0 # homeassistant.components.solax solax==3.2.3 @@ -2829,7 +2824,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.11 +spotifyaio==1.0.0 # homeassistant.components.sql sqlparse==0.5.0 @@ -2879,10 +2874,7 @@ switchbot-api==2.7.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==4.1.5 - -# homeassistant.components.system_bridge -systembridgemodels==4.2.4 +systembridgeconnector==4.1.10 # homeassistant.components.tailscale tailscale==0.6.2 @@ -2947,7 +2939,7 @@ thinqconnect==1.0.7 tikteck==0.4 # homeassistant.components.tilt_ble -tilt-ble==0.2.3 +tilt-ble==0.3.1 # homeassistant.components.tilt_pi tilt-pi==0.2.1 @@ -2958,6 +2950,9 @@ tmb==0.0.4 # homeassistant.components.todoist todoist-api-python==2.1.7 +# homeassistant.components.togrill +togrill-bluetooth==0.8.0 + # homeassistant.components.tolo tololib==1.2.2 @@ -2965,7 +2960,7 @@ tololib==1.2.2 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2025.1.4 +total-connect-client==2025.5 # homeassistant.components.tplink_lte tp-connected==0.0.4 @@ -3004,7 +2999,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.20.0 +uiprotect==7.21.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 @@ -3048,7 +3043,7 @@ vegehub==0.1.24 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2025.5.0 +velbus-aio==2025.8.0 # homeassistant.components.venstar venstarcolortouch==0.21 @@ -3167,7 +3162,7 @@ yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.11.1 +yalexs==8.12.0 # homeassistant.components.yeelight yeelight==0.7.16 @@ -3176,7 +3171,7 @@ yeelight==0.7.16 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.5.7 +yolink-api==0.5.8 # homeassistant.components.youless youless-api==2.2.0 @@ -3185,7 +3180,7 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.07.21 +yt-dlp[default]==2025.08.11 # homeassistant.components.zabbix zabbix-utils==2.0.2 @@ -3194,7 +3189,7 @@ zabbix-utils==2.0.2 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 @@ -3203,7 +3198,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.67 +zha==0.0.69 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test.txt b/requirements_test.txt index 592d4758340..9df62168b19 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -astroid==3.3.10 +astroid==3.3.11 coverage==7.10.0 freezegun==1.5.2 go2rtc-client==0.2.1 @@ -16,7 +16,7 @@ mock-open==1.4.0 mypy-dev==1.18.0a4 pre-commit==4.2.0 pydantic==2.11.7 -pylint==3.3.7 +pylint==3.3.8 pylint-per-file-ignores==1.4.0 pipdeptree==2.26.1 pytest-asyncio==1.1.0 @@ -35,19 +35,19 @@ requests-mock==1.12.1 respx==0.22.0 syrupy==4.9.1 tqdm==4.67.1 -types-aiofiles==24.1.0.20250708 +types-aiofiles==24.1.0.20250809 types-atomicwrites==1.4.5.1 -types-croniter==6.0.0.20250626 +types-croniter==6.0.0.20250809 types-caldav==1.3.0.20250516 types-chardet==0.1.5 types-decorator==5.2.0.20250324 -types-pexpect==4.9.0.20250516 -types-protobuf==6.30.2.20250703 -types-psutil==7.0.0.20250601 -types-pyserial==3.5.0.20250326 -types-python-dateutil==2.9.0.20250708 +types-pexpect==4.9.0.20250809 +types-protobuf==6.30.2.20250809 +types-psutil==7.0.0.20250801 +types-pyserial==3.5.0.20250809 +types-python-dateutil==2.9.0.20250809 types-python-slugify==8.0.2.20240310 -types-pytz==2025.2.0.20250516 -types-PyYAML==6.0.12.20250516 -types-requests==2.32.4.20250611 +types-pytz==2025.2.0.20250809 +types-PyYAML==6.0.12.20250809 +types-requests==2.32.4.20250809 types-xmltodict==0.13.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a75ee0920f2..325e07457ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -22,7 +22,7 @@ HAP-python==4.9.2 HATasmota==0.10.0 # homeassistant.components.mastodon -Mastodon.py==2.0.1 +Mastodon.py==2.1.2 # homeassistant.components.playstation_network PSNAWP==3.0.0 @@ -45,7 +45,7 @@ PlexAPI==4.15.16 ProgettiHWSW==0.1.3 # homeassistant.components.cast -PyChromecast==14.0.7 +PyChromecast==14.0.9 # homeassistant.components.flick_electric PyFlick==1.1.3 @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.68.3 +PySwitchbot==0.69.0 # homeassistant.components.syncthru PySyncThru==0.8.0 @@ -173,7 +173,7 @@ aioairzone-cloud==0.7.1 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==4.0.0 +aioamazondevices==5.0.0 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -195,7 +195,7 @@ aioasuswrt==1.4.0 aioautomower==2.1.2 # homeassistant.components.azure_devops -aioazuredevops==2.2.1 +aioazuredevops==2.2.2 # homeassistant.components.baf aiobafi6==0.9.0 @@ -208,7 +208,7 @@ aiobotocore==2.21.1 aiocomelit==0.12.3 # homeassistant.components.dhcp -aiodhcpwatcher==1.2.0 +aiodhcpwatcher==1.2.1 # homeassistant.components.dhcp aiodiscover==2.7.1 @@ -235,7 +235,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==37.2.2 +aioesphomeapi==39.0.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -250,7 +250,7 @@ aioguardian==2022.07.0 aioharmony==0.5.2 # homeassistant.components.hassio -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 # homeassistant.components.home_connect aiohomeconnect==0.18.1 @@ -289,7 +289,7 @@ aiolifx==1.2.1 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==2.0.1 +aiolyric==2.0.2 # homeassistant.components.mealie aiomealie==0.10.1 @@ -307,7 +307,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.3 +aiontfy==0.5.4 # homeassistant.components.nut aionut==4.3.4 @@ -357,7 +357,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.0 +aiorussound==4.8.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -399,7 +399,7 @@ aiotedee==0.2.25 aiotractive==0.6.0 # homeassistant.components.unifi -aiounifi==84 +aiounifi==86 # homeassistant.components.usb aiousbwatcher==1.1.1 @@ -420,7 +420,7 @@ aiowatttime==0.1.1 aiowebdav2==0.4.6 # homeassistant.components.webostv -aiowebostv==0.7.4 +aiowebostv==0.7.5 # homeassistant.components.withings aiowithings==3.1.6 @@ -435,7 +435,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.2.4 +airos==0.4.3 # homeassistant.components.airthings_ble airthings-ble==0.9.2 @@ -468,7 +468,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.52.0 +anthropic==0.62.0 # homeassistant.components.mcp_server anyio==4.9.0 @@ -491,6 +491,9 @@ aranet4==2.5.1 # homeassistant.components.arcam_fmj arcam-fmj==1.8.2 +# homeassistant.components.asuswrt +asusrouter==1.20.0 + # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms # homeassistant.components.samsungtv @@ -503,7 +506,7 @@ async-upnp-client==0.45.0 asyncarve==0.1.1 # homeassistant.components.sleepiq -asyncsleepiq==1.5.3 +asyncsleepiq==1.6.0 # homeassistant.components.aurora auroranoaa==0.0.5 @@ -559,7 +562,7 @@ bimmer-connected[china]==0.17.2 bleak-esphome==3.1.0 # homeassistant.components.bluetooth -bleak-retry-connector==4.0.0 +bleak-retry-connector==4.3.0 # homeassistant.components.bluetooth bleak==1.0.1 @@ -571,7 +574,7 @@ blebox-uniapi==2.5.0 blinkpy==0.23.0 # homeassistant.components.blue_current -bluecurrent-api==1.2.4 +bluecurrent-api==1.3.1 # homeassistant.components.bluemaestro bluemaestro-ble==0.4.1 @@ -598,7 +601,7 @@ bond-async==0.2.1 bosch-alarm-mode2==0.4.6 # homeassistant.components.bosch_shc -boschshcpy==0.2.91 +boschshcpy==0.2.107 # homeassistant.components.aws botocore==1.37.1 @@ -610,7 +613,7 @@ bring-api==1.1.0 broadlink==0.19.0 # homeassistant.components.brother -brother==5.0.0 +brother==5.0.1 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 @@ -633,9 +636,6 @@ caldav==1.6.0 # homeassistant.components.coinbase coinbase-advanced-py==1.2.2 -# homeassistant.components.coinbase -coinbase==2.1.0 - # homeassistant.scripts.check_config colorlog==6.9.0 @@ -646,7 +646,7 @@ colorthief==0.2.1 construct==2.10.68 # homeassistant.components.cookidoo -cookidoo-api==0.12.2 +cookidoo-api==0.14.0 # homeassistant.components.backup # homeassistant.components.utility_meter @@ -924,7 +924,7 @@ google-cloud-speech==2.31.1 google-cloud-texttospeech==2.25.1 # homeassistant.components.google_generative_ai_conversation -google-genai==1.7.0 +google-genai==1.29.0 # homeassistant.components.google_travel_time google-maps-routing==0.6.15 @@ -988,17 +988,17 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.1 +habiticalib==0.4.3 # homeassistant.components.bluetooth -habluetooth==4.0.2 +habluetooth==5.1.0 # homeassistant.components.cloud -hass-nabucasa==0.111.1 +hass-nabucasa==1.0.0 # homeassistant.components.assist_satellite # homeassistant.components.conversation -hassil==2.2.3 +hassil==3.1.0 # homeassistant.components.jewish_calendar hdate[astral]==1.1.2 @@ -1020,10 +1020,10 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.78 +holidays==0.79 # homeassistant.components.frontend -home-assistant-frontend==20250806.0 +home-assistant-frontend==20250811.1 # homeassistant.components.conversation home-assistant-intents==2025.7.30 @@ -1038,7 +1038,7 @@ httplib2==0.20.4 huawei-lte-api==1.11.0 # homeassistant.components.huum -huum==0.8.0 +huum==0.8.1 # homeassistant.components.hyperion hyperion-py==0.7.6 @@ -1074,7 +1074,7 @@ igloohome-api==0.1.1 imeon_inverter_api==0.3.14 # homeassistant.components.imgw_pib -imgw_pib==1.5.2 +imgw_pib==1.5.4 # homeassistant.components.incomfort incomfort-client==0.6.9 @@ -1129,7 +1129,7 @@ kegtron-ble==0.4.0 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.8.6.52906 +knx-frontend==2025.8.24.205840 # homeassistant.components.konnected konnected==1.2.0 @@ -1144,7 +1144,7 @@ lacrosse-view==1.1.1 laundrify-aio==1.2.2 # homeassistant.components.lcn -lcn-frontend==0.2.6 +lcn-frontend==0.2.7 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 @@ -1159,10 +1159,10 @@ led-ble==1.1.7 lektricowifi==0.1 # homeassistant.components.letpot -letpot==0.6.1 +letpot==0.6.2 # homeassistant.components.foscam -libpyfoscamcgi==0.0.6 +libpyfoscamcgi==0.0.7 # homeassistant.components.mikrotik librouteros==3.2.0 @@ -1170,9 +1170,6 @@ librouteros==3.2.0 # homeassistant.components.soundtouch libsoundtouch==0.8 -# homeassistant.components.linear_garage_door -linear-garage-door==0.2.9 - # homeassistant.components.livisi livisi==0.0.25 @@ -1189,7 +1186,7 @@ luftdaten==0.7.4 lupupy==0.3.2 # homeassistant.components.scrape -lxml==6.0.0 +lxml==6.0.1 # homeassistant.components.matrix matrix-nio==0.25.2 @@ -1301,7 +1298,7 @@ nextcloudmonitor==1.5.1 nextcord==3.1.0 # homeassistant.components.nextdns -nextdns==4.0.0 +nextdns==4.1.0 # homeassistant.components.niko_home_control nhc==0.4.12 @@ -1362,7 +1359,7 @@ ondilo==0.5.0 onedrive-personal-sdk==0.0.14 # homeassistant.components.onvif -onvif-zeep-async==4.0.2 +onvif-zeep-async==4.0.4 # homeassistant.components.opengarage open-garage==0.2.0 @@ -1372,7 +1369,7 @@ open-meteo==0.3.2 # homeassistant.components.open_router # homeassistant.components.openai_conversation -openai==1.93.3 +openai==1.99.5 # homeassistant.components.openerz openerz-api==0.3.0 @@ -1384,7 +1381,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.15.1 +opower==0.15.2 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1493,7 +1490,7 @@ py-dormakaba-dkey==1.0.6 py-improv-ble-client==1.0.3 # homeassistant.components.madvr -py-madvr2==1.6.32 +py-madvr2==1.6.40 # homeassistant.components.melissa py-melissa-climate==2.1.4 @@ -1550,14 +1547,11 @@ pyairvisual==2023.08.1 # homeassistant.components.aprilaire pyaprilaire==0.9.1 -# homeassistant.components.asuswrt -pyasuswrt==0.1.21 - # homeassistant.components.atag pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.2.1 +pyatmo==9.2.3 # homeassistant.components.apple_tv pyatv==0.16.1 @@ -1602,7 +1596,7 @@ pycsspeechtts==1.0.8 # pycups==2.0.4 # homeassistant.components.daikin -pydaikin==2.15.0 +pydaikin==2.16.0 # homeassistant.components.deako pydeako==0.6.0 @@ -1638,10 +1632,11 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms +# homeassistant.components.emoncms_history pyemoncms==0.1.2 # homeassistant.components.enphase_envoy -pyenphase==2.2.3 +pyenphase==2.3.0 # homeassistant.components.everlights pyeverlights==0.1.0 @@ -1788,13 +1783,13 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.2 +pymiele==0.5.4 # homeassistant.components.mochad pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.11.0 +pymodbus==3.11.1 # homeassistant.components.monoprice pymonoprice==0.4 @@ -1952,10 +1947,10 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smarla -pysmarlaapi==0.9.1 +pysmarlaapi==0.9.2 # homeassistant.components.smartthings -pysmartthings==3.2.8 +pysmartthings==3.2.9 # homeassistant.components.smarty pysmarty2==0.10.2 @@ -1967,7 +1962,7 @@ pysmhi==1.0.2 pysml==0.1.5 # homeassistant.components.smlight -pysmlight==0.2.7 +pysmlight==0.2.8 # homeassistant.components.snmp pysnmp==7.1.21 @@ -1988,7 +1983,7 @@ pyspeex-noise==1.0.2 pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron -pystiebeleltron==0.1.0 +pystiebeleltron==0.2.3 # homeassistant.components.suez_water pysuezV2==2.0.7 @@ -2039,7 +2034,7 @@ python-linkplay==0.2.12 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==8.0.0 +python-matter-server==8.1.0 # homeassistant.components.melcloud python-melcloud==0.1.0 @@ -2051,7 +2046,7 @@ python-miio==0.5.12 python-mpd2==3.1.1 # homeassistant.components.mystrom -python-mystrom==2.4.0 +python-mystrom==2.5.0 # homeassistant.components.open_router python-open-router==0.3.1 @@ -2082,7 +2077,7 @@ python-roborock==2.18.2 python-smarttub==0.0.44 # homeassistant.components.snoo -python-snoo==0.6.6 +python-snoo==0.8.3 # homeassistant.components.songpal python-songpal==0.16.2 @@ -2206,7 +2201,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 @@ -2239,7 +2234,7 @@ rova==0.4.1 rpi-bad-power==0.1.0 # homeassistant.components.ruuvitag_ble -ruuvitag-ble==0.1.2 +ruuvitag-ble==0.2.1 # homeassistant.components.yamaha rxv==0.7.0 @@ -2318,7 +2313,7 @@ snapcast==2.3.6 soco==0.30.11 # homeassistant.components.solarlog -solarlog_cli==0.4.0 +solarlog_cli==0.5.0 # homeassistant.components.solax solax==3.2.3 @@ -2336,7 +2331,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.11 +spotifyaio==1.0.0 # homeassistant.components.sql sqlparse==0.5.0 @@ -2377,10 +2372,7 @@ surepy==0.9.0 switchbot-api==2.7.0 # homeassistant.components.system_bridge -systembridgeconnector==4.1.5 - -# homeassistant.components.system_bridge -systembridgemodels==4.2.4 +systembridgeconnector==4.1.10 # homeassistant.components.tailscale tailscale==0.6.2 @@ -2427,7 +2419,7 @@ thermopro-ble==0.13.1 thinqconnect==1.0.7 # homeassistant.components.tilt_ble -tilt-ble==0.2.3 +tilt-ble==0.3.1 # homeassistant.components.tilt_pi tilt-pi==0.2.1 @@ -2435,6 +2427,9 @@ tilt-pi==0.2.1 # homeassistant.components.todoist todoist-api-python==2.1.7 +# homeassistant.components.togrill +togrill-bluetooth==0.8.0 + # homeassistant.components.tolo tololib==1.2.2 @@ -2442,7 +2437,7 @@ tololib==1.2.2 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2025.1.4 +total-connect-client==2025.5 # homeassistant.components.tplink_omada tplink-omada-client==1.4.4 @@ -2478,7 +2473,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.20.0 +uiprotect==7.21.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 @@ -2516,7 +2511,7 @@ vegehub==0.1.24 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2025.5.0 +velbus-aio==2025.8.0 # homeassistant.components.venstar venstarcolortouch==0.21 @@ -2617,13 +2612,13 @@ yalexs-ble==3.1.2 # homeassistant.components.august # homeassistant.components.yale -yalexs==8.11.1 +yalexs==8.12.0 # homeassistant.components.yeelight yeelight==0.7.16 # homeassistant.components.yolink -yolink-api==0.5.7 +yolink-api==0.5.8 # homeassistant.components.youless youless-api==2.2.0 @@ -2632,13 +2627,13 @@ youless-api==2.2.0 youtubeaio==2.0.0 # homeassistant.components.media_extractor -yt-dlp[default]==2025.07.21 +yt-dlp[default]==2025.08.11 # homeassistant.components.zamg zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 @@ -2647,7 +2642,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.67 +zha==0.0.69 # homeassistant.components.zwave_js zwave-js-server-python==0.67.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b13f586439d..9f65409b9be 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -170,7 +170,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==6.31.1 +protobuf==6.32.0 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder @@ -194,7 +194,7 @@ poetry==1000000000.0.0 # We want to skip the binary wheels for the 'charset-normalizer' packages. # They are build with mypyc, but causes issues with our wheel builder. # In order to do so, we need to constrain the version. -charset-normalizer==3.4.0 +charset-normalizer==3.4.3 # dacite: Ensure we have a version that is able to handle type unions for # NAM, Brother, and GIOS. @@ -238,6 +238,17 @@ multidict>=6.4.2 # rpds-py frequently updates cargo causing build failures # No wheels upstream available for armhf & armv7 rpds-py==0.26.0 + +# Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI +num2words==0.5.14 + +# pymodbus does not follow SemVer, and it keeps getting +# downgraded or upgraded by custom components +# This ensures all use the same version +pymodbus==3.11.1 + +# Some packages don't support gql 4.0.0 yet +gql<4.0.0 """ GENERATED_MESSAGE = ( diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 5776f6dfe12..6dbb086f273 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -14,7 +14,7 @@ WORKDIR "/github/workspace" COPY . /usr/src/homeassistant # Uv is only needed during build -RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ +RUN --mount=from=ghcr.io/astral-sh/uv:0.8.9,source=/uv,target=/bin/uv \ # Uv creates a lock file in /tmp --mount=type=tmpfs,target=/tmp \ # Required for PyTurboJPEG @@ -31,7 +31,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ PyTurboJPEG==1.8.0 \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ - hassil==2.2.3 \ + hassil==3.1.0 \ home-assistant-intents==2025.7.30 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 1d6db8e1f7a..6501aee0733 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -160,7 +160,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [ "anthropic", "aosmith", "apache_kafka", - "apcupsd", "apple_tv", "apprise", "aprilaire", @@ -713,7 +712,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [ "nuheat", "nuki", "numato", - "nut", "nws", "nx584", "nzbget", @@ -924,7 +922,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [ "somfy_mylink", "sonarr", "songpal", - "sonos", "sony_projector", "soundtouch", "spaceapi", @@ -1191,7 +1188,6 @@ INTEGRATIONS_WITHOUT_SCALE = [ "anthropic", "aosmith", "apache_kafka", - "apcupsd", "apple_tv", "apprise", "aprilaire", @@ -1655,7 +1651,6 @@ INTEGRATIONS_WITHOUT_SCALE = [ "manual", "manual_mqtt", "map", - "mastodon", "marytts", "matrix", "matter", @@ -1760,7 +1755,6 @@ INTEGRATIONS_WITHOUT_SCALE = [ "nuheat", "nuki", "numato", - "nut", "nws", "nx584", "nzbget", diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 9b5334823b9..ce5d1c78f60 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -3,14 +3,15 @@ from __future__ import annotations from collections import deque +from collections.abc import Collection from functools import cache -from importlib.metadata import metadata +from importlib.metadata import files, metadata import json import os import re import subprocess import sys -from typing import Any +from typing import Any, TypedDict from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from tqdm import tqdm @@ -94,15 +95,9 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # - reasonX should be the name of the invalid dependency "adax": {"adax": {"async-timeout"}, "adax-local": {"async-timeout"}}, "airthings": {"airthings-cloud": {"async-timeout"}}, - "alexa_devices": {"marisa-trie": {"setuptools"}}, "ampio": {"asmog": {"async-timeout"}}, "apache_kafka": {"aiokafka": {"async-timeout"}}, "apple_tv": {"pyatv": {"async-timeout"}}, - "azure_devops": { - # https://github.com/timmo001/aioazuredevops/issues/67 - # aioazuredevops > incremental > setuptools - "incremental": {"setuptools"} - }, "blackbird": { # https://github.com/koolsb/pyblackbird/issues/12 # pyblackbird > pyserial-asyncio @@ -114,11 +109,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # pycmus > pbr > setuptools "pbr": {"setuptools"} }, - "concord232": { - # https://bugs.launchpad.net/python-stevedore/+bug/2111694 - # concord232 > stevedore > pbr > setuptools - "pbr": {"setuptools"} - }, "delijn": {"pydelijn": {"async-timeout"}}, "devialet": {"async-upnp-client": {"async-timeout"}}, "dlna_dmr": {"async-upnp-client": {"async-timeout"}}, @@ -204,11 +194,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { "async-upnp-client": {"async-timeout"}, }, "loqed": {"loqedapi": {"async-timeout"}}, - "lyric": { - # https://github.com/timmo001/aiolyric/issues/115 - # aiolyric > incremental > setuptools - "incremental": {"setuptools"} - }, "matter": {"python-matter-server": {"async-timeout"}}, "mediaroom": {"pymediaroom": {"async-timeout"}}, "met": {"pymetno": {"async-timeout"}}, @@ -236,11 +221,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { }, "nibe_heatpump": {"nibe": {"async-timeout"}}, "norway_air": {"pymetno": {"async-timeout"}}, - "nx584": { - # https://bugs.launchpad.net/python-stevedore/+bug/2111694 - # pynx584 > stevedore > pbr > setuptools - "pbr": {"setuptools"} - }, "opengarage": {"open-garage": {"async-timeout"}}, "openhome": {"async-upnp-client": {"async-timeout"}}, "opensensemap": {"opensensemap-api": {"async-timeout"}}, @@ -271,11 +251,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { "squeezebox": {"pysqueezebox": {"async-timeout"}}, "ssdp": {"async-upnp-client": {"async-timeout"}}, "surepetcare": {"surepy": {"async-timeout"}}, - "system_bridge": { - # https://github.com/timmo001/system-bridge-connector/pull/78 - # systembridgeconnector > incremental > setuptools - "incremental": {"setuptools"} - }, "travisci": { # https://github.com/menegazzo/travispy seems to be unmaintained # and unused https://www.home-assistant.io/integrations/travisci @@ -300,6 +275,67 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { }, } +FORBIDDEN_FILE_NAMES: set[str] = { + "py.typed", # should be placed inside a package +} +FORBIDDEN_PACKAGE_NAMES: set[str] = { + "doc", + "docs", + "test", + "tests", +} +FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = { + # In the form dict("domain": {"package": {"reason1", "reason2"}}) + # - domain is the integration domain + # - package is the package (can be transitive) referencing the dependency + # - reasonX should be the name of the invalid dependency + # https://github.com/jaraco/jaraco.net + "abode": {"jaraco-abode": {"jaraco-net"}}, + # https://github.com/coinbase/coinbase-advanced-py + "coinbase": {"homeassistant": {"coinbase-advanced-py"}}, + # https://github.com/ggrammar/pizzapi + "dominos": {"homeassistant": {"pizzapi"}}, + # https://github.com/u9n/dlms-cosem + "dsmr": {"dsmr-parser": {"dlms-cosem"}}, + # https://github.com/ChrisMandich/PyFlume # Fixed with >=0.7.1 + "flume": {"homeassistant": {"pyflume"}}, + # https://github.com/fortinet-solutions-cse/fortiosapi + "fortios": {"homeassistant": {"fortiosapi"}}, + # https://github.com/manzanotti/geniushub-client + "geniushub": {"homeassistant": {"geniushub-client"}}, + # https://github.com/basnijholt/aiokef + "kef": {"homeassistant": {"aiokef"}}, + # https://github.com/danifus/pyzipper + "knx": {"xknxproject": {"pyzipper"}}, + # https://github.com/hthiery/python-lacrosse + "lacrosse": {"homeassistant": {"pylacrosse"}}, + # ??? + "linode": {"homeassistant": {"linode-api"}}, + # https://github.com/timmo001/aiolyric + "lyric": {"homeassistant": {"aiolyric"}}, + # https://github.com/microBeesTech/pythonSDK/ + "microbees": {"homeassistant": {"microbeespy"}}, + # https://github.com/tiagocoutinho/async_modbus + "nibe_heatpump": {"nibe": {"async-modbus"}}, + # https://github.com/ejpenney/pyobihai + "obihai": {"homeassistant": {"pyobihai"}}, + # https://github.com/iamkubi/pydactyl + "pterodactyl": {"homeassistant": {"py-dactyl"}}, + # https://github.com/markusressel/raspyrfm-client + "raspyrfm": {"homeassistant": {"raspyrfm-client"}}, + # https://github.com/sstallion/sensorpush-api + "sensorpush_cloud": { + "homeassistant": {"sensorpush-api"}, + "sensorpush-ha": {"sensorpush-api"}, + }, + # https://github.com/smappee/pysmappee + "smappee": {"homeassistant": {"pysmappee"}}, + # https://github.com/watergate-ai/watergate-local-api-python + "watergate": {"homeassistant": {"watergate-local-api"}}, + # https://github.com/markusressel/xs1-api-client + "xs1": {"homeassistant": {"xs1-api-client"}}, +} + PYTHON_VERSION_CHECK_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # In the form dict("domain": {"package": {"dependency1", "dependency2"}}) # - domain is the integration domain @@ -312,6 +348,16 @@ PYTHON_VERSION_CHECK_EXCEPTIONS: dict[str, dict[str, set[str]]] = { } +class _PackageFilesCheckResult(TypedDict): + """Data structure to store results of package files check.""" + + top_level: set[str] + file_names: set[str] + + +_packages_checked_files_cache: dict[str, _PackageFilesCheckResult] = {} + + def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle requirements for integrations.""" # Check if we are doing format-only validation. @@ -476,6 +522,12 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: ) needs_forbidden_package_exceptions = False + packages_checked_files: set[str] = set() + forbidden_package_files_exceptions = FORBIDDEN_PACKAGE_FILES_EXCEPTIONS.get( + integration.domain, {} + ) + needs_forbidden_package_files_exception = False + package_version_check_exceptions = PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS.get( integration.domain, {} ) @@ -517,6 +569,17 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: f"({requires_python}) in {package}", ) + # Check package names + if package not in packages_checked_files: + packages_checked_files.add(package) + if not check_dependency_files( + integration, + "homeassistant", + package, + forbidden_package_files_exceptions.get("homeassistant", ()), + ): + needs_forbidden_package_files_exception = True + # Use inner loop to check dependencies # so we have access to the dependency parent (=current package) dependencies: dict[str, str] = item["dependencies"] @@ -540,6 +603,17 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: ): needs_package_version_check_exception = True + # Check package names + if pkg not in packages_checked_files: + packages_checked_files.add(pkg) + if not check_dependency_files( + integration, + package, + pkg, + forbidden_package_files_exceptions.get(package, ()), + ): + needs_forbidden_package_files_exception = True + to_check.extend(dependencies) if forbidden_package_exceptions and not needs_forbidden_package_exceptions: @@ -560,6 +634,15 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: f"Integration {integration.domain} version restrictions for Python have " "been resolved, please remove from `PYTHON_VERSION_CHECK_EXCEPTIONS`", ) + if ( + forbidden_package_files_exceptions + and not needs_forbidden_package_files_exception + ): + integration.add_error( + "requirements", + f"Integration {integration.domain} runtime files dependency exceptions " + "have been resolved, please remove from `FORBIDDEN_PACKAGE_FILES_EXCEPTIONS`", + ) return all_requirements @@ -635,6 +718,43 @@ def _is_dependency_version_range_valid( return False +def check_dependency_files( + integration: Integration, + package: str, + pkg: str, + package_exceptions: Collection[str], +) -> bool: + """Check dependency files for forbidden files and forbidden package names.""" + if (results := _packages_checked_files_cache.get(pkg)) is None: + top_level: set[str] = set() + file_names: set[str] = set() + for file in files(pkg) or (): + if not (top := file.parts[0].lower()).endswith((".dist-info", ".py")): + top_level.add(top) + if (name := str(file)).lower() in FORBIDDEN_FILE_NAMES: + file_names.add(name) + results = _PackageFilesCheckResult( + top_level=FORBIDDEN_PACKAGE_NAMES & top_level, + file_names=file_names, + ) + _packages_checked_files_cache[pkg] = results + if not (results["top_level"] or results["file_names"]): + return True + + for dir_name in results["top_level"]: + integration.add_warning_or_error( + pkg in package_exceptions, + "requirements", + f"Package {pkg} has a forbidden top level directory '{dir_name}' in {package}", + ) + for file_name in results["file_names"]: + integration.add_error( + "requirements", + f"Package {pkg} has a forbidden file '{file_name}' in {package}", + ) + return False + + def install_requirements(integration: Integration, requirements: set[str]) -> bool: """Install integration requirements. diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index d09fb27f71a..e29967d6716 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -334,12 +334,11 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: slug_validator=translation_key_validator, ), }, - vol.Optional("config_panel"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys( + vol.Optional("config_panel"): vol.Schema( + vol.Any( + {vol.Any(translation_key_validator, "_"): vol.Self}, translation_value_validator, - slug_validator=translation_key_validator, - ), - slug_validator=vol.Any("_", cv.slug), + ) ), vol.Optional("application_credentials"): { vol.Optional("description"): translation_value_validator, diff --git a/script/install_integration_requirements.py b/script/install_integration_requirements.py index 91c9f6a8ed0..74fd1c93be5 100644 --- a/script/install_integration_requirements.py +++ b/script/install_integration_requirements.py @@ -1,4 +1,4 @@ -"""Install requirements for a given integration.""" +"""Install requirements for one or more integrations.""" import argparse from pathlib import Path @@ -12,39 +12,49 @@ from .util import valid_integration def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" parser = argparse.ArgumentParser( - description="Install requirements for a given integration" + description="Install requirements for one or more integrations" ) parser.add_argument( - "integration", type=valid_integration, help="Integration to target." + "integrations", + nargs="+", + type=valid_integration, + help="Integration(s) to target.", ) return parser.parse_args() def main() -> int | None: - """Install requirements for a given integration.""" + """Install requirements for the specified integrations.""" if not Path("requirements_all.txt").is_file(): print("Run from project root") return 1 args = get_arguments() - requirements = gather_recursive_requirements(args.integration) + # Gather requirements for all specified integrations + all_requirements = set() + for integration in args.integrations: + requirements = gather_recursive_requirements(integration) + all_requirements.update(requirements) - cmd = [ - "uv", - "pip", - "install", - "-c", - "homeassistant/package_constraints.txt", - "-U", - *requirements, - ] - print(" ".join(cmd)) - subprocess.run( - cmd, - check=True, - ) + if all_requirements: + cmd = [ + "uv", + "pip", + "install", + "-c", + "homeassistant/package_constraints.txt", + "-U", + *sorted(all_requirements), # Sort for consistent output + ] + print(" ".join(cmd)) + subprocess.run( + cmd, + check=True, + ) + else: + print("No requirements to install.") return None diff --git a/script/licenses.py b/script/licenses.py index d7819cba536..ef62d4970dd 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -202,6 +202,7 @@ EXCEPTIONS = { "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 "sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14 "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 + "ujson", # https://github.com/ultrajson/ultrajson/blob/main/LICENSE.txt } # fmt: off diff --git a/tests/components/ai_task/snapshots/test_task.ambr b/tests/components/ai_task/snapshots/test_task.ambr index 181fc383d64..6986c12f8b7 100644 --- a/tests/components/ai_task/snapshots/test_task.ambr +++ b/tests/components/ai_task/snapshots/test_task.ambr @@ -16,7 +16,9 @@ dict({ 'agent_id': 'ai_task.test_task_entity', 'content': 'Mock result', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index 575c596404b..e205e626ab8 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -622,7 +622,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-pm01', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[indoor][sensor.airgradient_pm1-state] @@ -631,7 +631,7 @@ 'device_class': 'pm1', 'friendly_name': 'Airgradient PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airgradient_pm1', @@ -675,7 +675,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-pm10', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[indoor][sensor.airgradient_pm10-state] @@ -684,7 +684,7 @@ 'device_class': 'pm10', 'friendly_name': 'Airgradient PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airgradient_pm10', @@ -728,7 +728,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-pm02', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[indoor][sensor.airgradient_pm2_5-state] @@ -737,7 +737,7 @@ 'device_class': 'pm25', 'friendly_name': 'Airgradient PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airgradient_pm2_5', @@ -833,7 +833,7 @@ 'supported_features': 0, 'translation_key': 'raw_pm02', 'unique_id': '84fce612f5b8-pm02_raw', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[indoor][sensor.airgradient_raw_pm2_5-state] @@ -842,7 +842,7 @@ 'device_class': 'pm25', 'friendly_name': 'Airgradient Raw PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airgradient_raw_pm2_5', diff --git a/tests/components/airly/snapshots/test_sensor.ambr b/tests/components/airly/snapshots/test_sensor.ambr index efd809e76ae..8d79f8cdf0a 100644 --- a/tests/components/airly/snapshots/test_sensor.ambr +++ b/tests/components/airly/snapshots/test_sensor.ambr @@ -36,7 +36,7 @@ 'supported_features': 0, 'translation_key': 'co', 'unique_id': '123-456-co', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_carbon_monoxide-state] @@ -47,7 +47,7 @@ 'limit': 4000, 'percent': 4, 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_carbon_monoxide', @@ -207,7 +207,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-no2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_nitrogen_dioxide-state] @@ -219,7 +219,7 @@ 'limit': 25, 'percent': 64, 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_nitrogen_dioxide', @@ -266,7 +266,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-o3', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_ozone-state] @@ -278,7 +278,7 @@ 'limit': 100, 'percent': 42, 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_ozone', @@ -325,7 +325,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-pm1', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_pm1-state] @@ -335,7 +335,7 @@ 'device_class': 'pm1', 'friendly_name': 'Home PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_pm1', @@ -382,7 +382,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-pm10', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_pm10-state] @@ -394,7 +394,7 @@ 'limit': 45, 'percent': 14, 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_pm10', @@ -441,7 +441,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-pm25', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_pm2_5-state] @@ -453,7 +453,7 @@ 'limit': 15, 'percent': 29, 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_pm2_5', @@ -557,7 +557,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-so2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_sulphur_dioxide-state] @@ -569,7 +569,7 @@ 'limit': 40, 'percent': 35, 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_sulphur_dioxide', diff --git a/tests/components/airos/__init__.py b/tests/components/airos/__init__.py index 8c6182a8650..f663644a8a4 100644 --- a/tests/components/airos/__init__.py +++ b/tests/components/airos/__init__.py @@ -1,13 +1,19 @@ """Tests for the Ubiquity airOS integration.""" +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, patch -async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: +async def setup_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + platforms: list[Platform] | None = None, +) -> None: """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + with patch("homeassistant.components.airos._PLATFORMS", platforms): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/airos/conftest.py b/tests/components/airos/conftest.py index b17908e801a..5443f79a976 100644 --- a/tests/components/airos/conftest.py +++ b/tests/components/airos/conftest.py @@ -15,7 +15,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture def ap_fixture(): """Load fixture data for AP mode.""" - json_data = load_json_object_fixture("airos_ap-ptp.json", DOMAIN) + json_data = load_json_object_fixture("airos_loco5ac_ap-ptp.json", DOMAIN) return AirOSData.from_dict(json_data) diff --git a/tests/components/airos/fixtures/airos_ap-ptp.json b/tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json similarity index 79% rename from tests/components/airos/fixtures/airos_ap-ptp.json rename to tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json index 06d13ba1101..06feb3d0a55 100644 --- a/tests/components/airos/fixtures/airos_ap-ptp.json +++ b/tests/components/airos/fixtures/airos_loco5ac_ap-ptp.json @@ -1,132 +1,201 @@ { "chain_names": [ - { "number": 1, "name": "Chain 0" }, - { "number": 2, "name": "Chain 1" } + { + "name": "Chain 0", + "number": 1 + }, + { + "name": "Chain 1", + "number": 2 + } ], - "host": { - "hostname": "NanoStation 5AC ap name", - "device_id": "03aa0d0b40fed0a47088293584ef5432", - "uptime": 264888, - "power_time": 268683, - "time": "2025-06-23 23:06:42", - "timestamp": 2668313184, - "fwversion": "v8.7.17", - "devmodel": "NanoStation 5AC loco", - "netrole": "bridge", - "loadavg": 0.412598, - "totalram": 63447040, - "freeram": 16564224, - "temperature": 0, - "cpuload": 10.10101, - "height": 3 - }, - "genuine": "/images/genuine.png", - "services": { - "dhcpc": false, - "dhcpd": false, - "dhcp6d_stateful": false, - "pppoe": false, - "airview": 2 + "derived": { + "access_point": true, + "mac": "01:23:45:67:89:AB", + "mac_interface": "br0", + "mode": "point_to_point", + "ptmp": false, + "ptp": true, + "role": "access_point", + "station": false }, "firewall": { - "iptables": false, + "eb6tables": false, "ebtables": false, "ip6tables": false, - "eb6tables": false + "iptables": false }, + "genuine": "/images/genuine.png", + "gps": { + "alt": null, + "dim": null, + "dop": null, + "fix": 0, + "lat": 52.379894, + "lon": 4.901608, + "sats": null, + "time_synced": null + }, + "host": { + "cpuload": 10.10101, + "device_id": "03aa0d0b40fed0a47088293584ef5432", + "devmodel": "NanoStation 5AC loco", + "freeram": 16564224, + "fwversion": "v8.7.17", + "height": 3, + "hostname": "NanoStation 5AC ap name", + "loadavg": 0.412598, + "netrole": "bridge", + "power_time": 268683, + "temperature": 0, + "time": "2025-06-23 23:06:42", + "timestamp": 2668313184, + "totalram": 63447040, + "uptime": 264888 + }, + "interfaces": [ + { + "enabled": true, + "hwaddr": "01:23:45:67:89:AB", + "ifname": "eth0", + "mtu": 1500, + "status": { + "cable_len": 18, + "duplex": true, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": true, + "rx_bytes": 3984971949, + "rx_dropped": 0, + "rx_errors": 4, + "rx_packets": 73564835, + "snr": [30, 30, 30, 30], + "speed": 1000, + "tx_bytes": 209900085624, + "tx_dropped": 10, + "tx_errors": 0, + "tx_packets": 185866883 + } + }, + { + "enabled": true, + "hwaddr": "01:23:45:67:89:AB", + "ifname": "ath0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": null, + "ipaddr": "0.0.0.0", + "plugged": false, + "rx_bytes": 206938324766, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 149767200, + "snr": null, + "speed": 0, + "tx_bytes": 5265602738, + "tx_dropped": 2005, + "tx_errors": 0, + "tx_packets": 52980390 + } + }, + { + "enabled": true, + "hwaddr": "01:23:45:67:89:AB", + "ifname": "br0", + "mtu": 1500, + "status": { + "cable_len": null, + "duplex": false, + "ip6addr": [ + { + "addr": "fe80::eea:14ff:fea4:89cd", + "plen": 64 + } + ], + "ipaddr": "192.168.1.2", + "plugged": true, + "rx_bytes": 204802727, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 1791592, + "snr": null, + "speed": 0, + "tx_bytes": 236295176, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 298119 + } + } + ], + "ntpclient": {}, "portfw": false, + "provmode": {}, + "services": { + "airview": 2, + "dhcp6d_stateful": false, + "dhcpc": false, + "dhcpd": false, + "pppoe": false + }, + "unms": { + "status": 0, + "timestamp": null + }, "wireless": { - "essid": "DemoSSID", - "mode": "ap-ptp", - "ieeemode": "11ACVHT80", - "band": 2, - "compat_11n": 0, - "hide_essid": 0, - "apmac": "01:23:45:67:89:AB", "antenna_gain": 13, - "frequency": 5500, - "center1_freq": 5530, - "dfs": 1, - "distance": 0, - "security": "WPA2", - "noisef": -89, - "txpower": -3, + "apmac": "01:23:45:67:89:AB", "aprepeater": false, - "rstatus": 5, - "chanbw": 80, - "rx_chainmask": 3, - "tx_chainmask": 3, - "nol_state": 0, - "nol_timeout": 0, + "band": 2, "cac_state": 0, "cac_timeout": 0, - "rx_idx": 8, - "rx_nss": 2, - "tx_idx": 9, - "tx_nss": 2, - "throughput": { "tx": 222, "rx": 9907 }, - "service": { "time": 267181, "link": 266003 }, + "center1_freq": 5530, + "chanbw": 80, + "compat_11n": 0, + "count": 1, + "dfs": 1, + "distance": 0, + "essid": "DemoSSID", + "frequency": 5500, + "hide_essid": 0, + "ieeemode": "11ACVHT80", + "mode": "ap-ptp", + "noisef": -89, + "nol_state": 0, + "nol_timeout": 0, "polling": { + "atpc_status": 2, "cb_capacity": 593970, "dl_capacity": 647400, - "ul_capacity": 540540, - "use": 48, - "tx_use": 6, - "rx_use": 42, - "atpc_status": 2, + "ff_cap_rep": false, "fixed_frame": false, + "flex_mode": null, "gps_sync": false, - "ff_cap_rep": false + "rx_use": 42, + "tx_use": 6, + "ul_capacity": 540540, + "use": 48 + }, + "rstatus": 5, + "rx_chainmask": 3, + "rx_idx": 8, + "rx_nss": 2, + "security": "WPA2", + "service": { + "link": 266003, + "time": 267181 }, - "count": 1, "sta": [ { - "mac": "01:23:45:67:89:AB", - "lastip": "192.168.1.2", - "signal": -59, - "rssi": 37, - "noisefloor": -89, - "chainrssi": [35, 32, 0], - "tx_idx": 9, - "rx_idx": 8, - "tx_nss": 2, - "rx_nss": 2, - "tx_latency": 0, - "distance": 1, - "tx_packets": 0, - "tx_lretries": 0, - "tx_sretries": 0, - "uptime": 170281, - "dl_signal_expect": -80, - "ul_signal_expect": -55, - "cb_capacity_expect": 416000, - "dl_capacity_expect": 208000, - "ul_capacity_expect": 624000, - "dl_rate_expect": 3, - "ul_rate_expect": 8, - "dl_linkscore": 100, - "ul_linkscore": 86, - "dl_avg_linkscore": 100, - "ul_avg_linkscore": 88, - "tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430], - "stats": { - "rx_bytes": 206938324814, - "rx_packets": 149767200, - "rx_pps": 846, - "tx_bytes": 5265602739, - "tx_packets": 52980390, - "tx_pps": 0 - }, "airmax": { "actual_priority": 0, - "beam": 0, - "desired_priority": 0, - "cb_capacity": 593970, - "dl_capacity": 647400, - "ul_capacity": 540540, "atpc_status": 2, + "beam": 0, + "cb_capacity": 593970, + "desired_priority": 0, + "dl_capacity": 647400, "rx": { - "usage": 42, "cinr": 31, "evm": [ [ @@ -141,10 +210,10 @@ 34, 33, 34, 34, 34, 34, 34, 35, 35, 35, 34, 35, 33, 34, 34, 34, 34, 35, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35 ] - ] + ], + "usage": 42 }, "tx": { - "usage": 6, "cinr": 31, "evm": [ [ @@ -159,142 +228,127 @@ 38, 37, 37, 37, 38, 37, 38, 37, 37, 37, 37, 37, 36, 37, 37, 37, 37, 37, 37, 38, 37, 37, 38, 37, 36, 37, 37, 37, 37, 37, 37, 37 ] - ] - } + ], + "usage": 6 + }, + "ul_capacity": 540540 }, + "airos_connected": true, + "cb_capacity_expect": 416000, + "chainrssi": [35, 32, 0], + "distance": 1, + "dl_avg_linkscore": 100, + "dl_capacity_expect": 208000, + "dl_linkscore": 100, + "dl_rate_expect": 3, + "dl_signal_expect": -80, "last_disc": 1, + "lastip": "192.168.1.2", + "mac": "01:23:45:67:89:AB", + "noisefloor": -89, "remote": { "age": 1, - "device_id": "d4f4cdf82961e619328a8f72f8d7653b", - "hostname": "NanoStation 5AC sta name", - "platform": "NanoStation 5AC loco", - "version": "WA.ar934x.v8.7.17.48152.250620.2132", - "time": "2025-06-23 23:13:54", - "cpuload": 43.564301, - "temperature": 0, - "totalram": 63447040, - "freeram": 14290944, - "netrole": "bridge", - "mode": "sta-ptp", - "sys_id": "0xe7fa", - "tx_throughput": 16023, - "rx_throughput": 251, - "uptime": 265320, - "power_time": 268512, - "compat_11n": 0, - "signal": -58, - "rssi": 38, - "noisefloor": -90, - "tx_power": -4, - "distance": 1, - "rx_chainmask": 3, + "airview": 2, + "antenna_gain": 13, + "cable_loss": 0, "chainrssi": [33, 37, 0], + "compat_11n": 0, + "cpuload": 43.564301, + "device_id": "d4f4cdf82961e619328a8f72f8d7653b", + "distance": 1, + "ethlist": [ + { + "cable_len": 14, + "duplex": true, + "enabled": true, + "ifname": "eth0", + "plugged": true, + "snr": [30, 30, 29, 30], + "speed": 1000 + } + ], + "freeram": 14290944, + "gps": { + "alt": null, + "dim": null, + "dop": null, + "fix": 0, + "lat": 52.379894, + "lon": 4.901608, + "sats": null, + "time_synced": null + }, + "height": 2, + "hostname": "NanoStation 5AC sta name", + "ip6addr": ["fe80::eea:14ff:fea4:89ab"], + "ipaddr": ["192.168.1.2"], + "mode": "sta-ptp", + "netrole": "bridge", + "noisefloor": -90, + "oob": false, + "platform": "NanoStation 5AC loco", + "power_time": 268512, + "rssi": 38, + "rx_bytes": 3624206478, + "rx_chainmask": 3, + "rx_throughput": 251, + "service": { + "link": 265996, + "time": 267195 + }, + "signal": -58, + "sys_id": "0xe7fa", + "temperature": 0, + "time": "2025-06-23 23:13:54", + "totalram": 63447040, + "tx_bytes": 212308148210, + "tx_power": -4, "tx_ratedata": [ 14, 4, 372, 2223, 4708, 4037, 8142, 485763, 29420892, 24748154 ], - "tx_bytes": 212308148210, - "rx_bytes": 3624206478, - "antenna_gain": 13, - "cable_loss": 0, - "height": 2, - "ethlist": [ - { - "ifname": "eth0", - "enabled": true, - "plugged": true, - "duplex": true, - "speed": 1000, - "snr": [30, 30, 29, 30], - "cable_len": 14 - } - ], - "ipaddr": ["192.168.1.2"], - "ip6addr": ["fe80::eea:14ff:fea4:89ab"], - "gps": { "lat": "52.379894", "lon": "4.901608", "fix": 0 }, - "oob": false, - "unms": { "status": 0, "timestamp": null }, - "airview": 2, - "service": { "time": 267195, "link": 265996 } + "tx_throughput": 16023, + "unms": { + "status": 0, + "timestamp": null + }, + "uptime": 265320, + "version": "WA.ar934x.v8.7.17.48152.250620.2132" }, - "airos_connected": true + "rssi": 37, + "rx_idx": 8, + "rx_nss": 2, + "signal": -59, + "stats": { + "rx_bytes": 206938324814, + "rx_packets": 149767200, + "rx_pps": 846, + "tx_bytes": 5265602739, + "tx_packets": 52980390, + "tx_pps": 0 + }, + "tx_idx": 9, + "tx_latency": 0, + "tx_lretries": 0, + "tx_nss": 2, + "tx_packets": 0, + "tx_ratedata": [175, 4, 47, 200, 673, 158, 163, 138, 68895, 19577430], + "tx_sretries": 0, + "ul_avg_linkscore": 88, + "ul_capacity_expect": 624000, + "ul_linkscore": 86, + "ul_rate_expect": 8, + "ul_signal_expect": -55, + "uptime": 170281 } ], - "sta_disconnected": [] - }, - "interfaces": [ - { - "ifname": "eth0", - "hwaddr": "01:23:45:67:89:AB", - "enabled": true, - "mtu": 1500, - "status": { - "plugged": true, - "tx_bytes": 209900085624, - "rx_bytes": 3984971949, - "tx_packets": 185866883, - "rx_packets": 73564835, - "tx_errors": 0, - "rx_errors": 4, - "tx_dropped": 10, - "rx_dropped": 0, - "ipaddr": "0.0.0.0", - "speed": 1000, - "duplex": true, - "snr": [30, 30, 30, 30], - "cable_len": 18, - "ip6addr": null - } + "sta_disconnected": [], + "throughput": { + "rx": 9907, + "tx": 222 }, - { - "ifname": "ath0", - "hwaddr": "01:23:45:67:89:AB", - "enabled": true, - "mtu": 1500, - "status": { - "plugged": false, - "tx_bytes": 5265602738, - "rx_bytes": 206938324766, - "tx_packets": 52980390, - "rx_packets": 149767200, - "tx_errors": 0, - "rx_errors": 0, - "tx_dropped": 2005, - "rx_dropped": 0, - "ipaddr": "0.0.0.0", - "speed": 0, - "duplex": false, - "snr": null, - "cable_len": null, - "ip6addr": null - } - }, - { - "ifname": "br0", - "hwaddr": "01:23:45:67:89:AB", - "enabled": true, - "mtu": 1500, - "status": { - "plugged": true, - "tx_bytes": 236295176, - "rx_bytes": 204802727, - "tx_packets": 298119, - "rx_packets": 1791592, - "tx_errors": 0, - "rx_errors": 0, - "tx_dropped": 0, - "rx_dropped": 0, - "ipaddr": "192.168.1.2", - "speed": 0, - "duplex": false, - "snr": null, - "cable_len": null, - "ip6addr": [{ "addr": "fe80::eea:14ff:fea4:89cd", "plen": 64 }] - } - } - ], - "provmode": {}, - "ntpclient": {}, - "unms": { "status": 0, "timestamp": null }, - "gps": { "lat": 52.379894, "lon": 4.901608, "fix": 0 }, - "derived": { "mac": "01:23:45:67:89:AB", "mac_interface": "br0" } + "tx_chainmask": 3, + "tx_idx": 9, + "tx_nss": 2, + "txpower": -3 + } } diff --git a/tests/components/airos/snapshots/test_binary_sensor.ambr b/tests/components/airos/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..d9815e0c62b --- /dev/null +++ b/tests/components/airos/snapshots/test_binary_sensor.ambr @@ -0,0 +1,245 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_client-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_client', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHCP client', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dhcp_client', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp_client', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_client-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'NanoStation 5AC ap name DHCP client', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_client', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_server-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_server', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHCP server', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dhcp_server', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp_server', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcp_server-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'NanoStation 5AC ap name DHCP server', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcp_server', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcpv6_server-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcpv6_server', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHCPv6 server', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dhcp6_server', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_dhcp6_server', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_dhcpv6_server-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'NanoStation 5AC ap name DHCPv6 server', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_dhcpv6_server', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_port_forwarding-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_port_forwarding', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Port forwarding', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'port_forwarding', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_portfw', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_port_forwarding-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NanoStation 5AC ap name Port forwarding', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_port_forwarding', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_pppoe_link-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_pppoe_link', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PPPoE link', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pppoe', + 'unique_id': '03aa0d0b40fed0a47088293584ef5432_pppoe', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.nanostation_5ac_ap_name_pppoe_link-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'NanoStation 5AC ap name PPPoE link', + }), + 'context': , + 'entity_id': 'binary_sensor.nanostation_5ac_ap_name_pppoe_link', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr index bc2dedc905a..f4561ec6d99 100644 --- a/tests/components/airos/snapshots/test_diagnostics.ambr +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -13,8 +13,14 @@ }), ]), 'derived': dict({ + 'access_point': True, 'mac': '**REDACTED**', 'mac_interface': 'br0', + 'mode': 'point_to_point', + 'ptmp': False, + 'ptp': True, + 'role': 'access_point', + 'station': False, }), 'firewall': dict({ 'eb6tables': False, @@ -24,9 +30,14 @@ }), 'genuine': '/images/genuine.png', 'gps': dict({ + 'alt': None, + 'dim': None, + 'dop': None, 'fix': 0, 'lat': '**REDACTED**', 'lon': '**REDACTED**', + 'sats': None, + 'time_synced': None, }), 'host': dict({ 'cpuload': 10.10101, @@ -164,6 +175,7 @@ 'dl_capacity': 647400, 'ff_cap_rep': False, 'fixed_frame': False, + 'flex_mode': None, 'gps_sync': False, 'rx_use': 42, 'tx_use': 6, @@ -515,9 +527,14 @@ ]), 'freeram': 14290944, 'gps': dict({ + 'alt': None, + 'dim': None, + 'dop': None, 'fix': 0, 'lat': '**REDACTED**', 'lon': '**REDACTED**', + 'sats': None, + 'time_synced': None, }), 'height': 2, 'hostname': '**REDACTED**', diff --git a/tests/components/airos/snapshots/test_sensor.ambr b/tests/components/airos/snapshots/test_sensor.ambr index e414d35beb2..815b11ddc7e 100644 --- a/tests/components/airos/snapshots/test_sensor.ambr +++ b/tests/components/airos/snapshots/test_sensor.ambr @@ -76,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, @@ -131,6 +134,9 @@ 'sensor': dict({ 'suggested_display_precision': 0, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -141,7 +147,7 @@ 'supported_features': 0, 'translation_key': 'wireless_polling_dl_capacity', 'unique_id': '01:23:45:67:89:AB_wireless_polling_dl_capacity', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_download_capacity-state] @@ -150,14 +156,14 @@ 'device_class': 'data_rate', 'friendly_name': 'NanoStation 5AC ap name Download capacity', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.nanostation_5ac_ap_name_download_capacity', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '647400', + 'state': '647.4', }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_network_role-entry] @@ -245,6 +251,9 @@ 'sensor': dict({ 'suggested_display_precision': 0, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -255,7 +264,7 @@ 'supported_features': 0, 'translation_key': 'wireless_throughput_rx', 'unique_id': '01:23:45:67:89:AB_wireless_throughput_rx', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_receive_actual-state] @@ -264,14 +273,14 @@ 'device_class': 'data_rate', 'friendly_name': 'NanoStation 5AC ap name Throughput receive (actual)', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_receive_actual', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '9907', + 'state': '9.907', }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_transmit_actual-entry] @@ -301,6 +310,9 @@ 'sensor': dict({ 'suggested_display_precision': 0, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -311,7 +323,7 @@ 'supported_features': 0, 'translation_key': 'wireless_throughput_tx', 'unique_id': '01:23:45:67:89:AB_wireless_throughput_tx', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_throughput_transmit_actual-state] @@ -320,14 +332,14 @@ 'device_class': 'data_rate', 'friendly_name': 'NanoStation 5AC ap name Throughput transmit (actual)', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.nanostation_5ac_ap_name_throughput_transmit_actual', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '222', + 'state': '0.222', }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_upload_capacity-entry] @@ -357,6 +369,9 @@ 'sensor': dict({ 'suggested_display_precision': 0, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -367,7 +382,7 @@ 'supported_features': 0, 'translation_key': 'wireless_polling_ul_capacity', 'unique_id': '01:23:45:67:89:AB_wireless_polling_ul_capacity', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_upload_capacity-state] @@ -376,14 +391,126 @@ 'device_class': 'data_rate', 'friendly_name': 'NanoStation 5AC ap name Upload capacity', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.nanostation_5ac_ap_name_upload_capacity', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '540540', + 'state': '540.54', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'host_uptime', + 'unique_id': '01:23:45:67:89:AB_host_uptime', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'NanoStation 5AC ap name Uptime', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.06583333333333', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_distance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wireless distance', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_distance', + 'unique_id': '01:23:45:67:89:AB_wireless_distance', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'NanoStation 5AC ap name Wireless distance', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', }) # --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_frequency-entry] @@ -439,6 +566,122 @@ 'state': '5500', }) # --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'point_to_point', + 'point_to_multipoint', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wireless mode', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_mode', + 'unique_id': '01:23:45:67:89:AB_wireless_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'NanoStation 5AC ap name Wireless mode', + 'options': list([ + 'point_to_point', + 'point_to_multipoint', + ]), + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'point_to_point', + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_role-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'station', + 'access_point', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_role', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wireless role', + 'platform': 'airos', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wireless_role', + 'unique_id': '01:23:45:67:89:AB_wireless_role', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_role-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'NanoStation 5AC ap name Wireless role', + 'options': list([ + 'station', + 'access_point', + ]), + }), + 'context': , + 'entity_id': 'sensor.nanostation_5ac_ap_name_wireless_role', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'access_point', + }) +# --- # name: test_all_entities[sensor.nanostation_5ac_ap_name_wireless_ssid-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/airos/test_binary_sensor.py b/tests/components/airos/test_binary_sensor.py new file mode 100644 index 00000000000..40c3d631cd3 --- /dev/null +++ b/tests/components/airos/test_binary_sensor.py @@ -0,0 +1,28 @@ +"""Test the Ubiquiti airOS binary sensors.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR]) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/airos/test_sensor.py b/tests/components/airos/test_sensor.py index c9e675e7987..7f39f504753 100644 --- a/tests/components/airos/test_sensor.py +++ b/tests/components/airos/test_sensor.py @@ -13,7 +13,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.airos.const import SCAN_INTERVAL -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -31,7 +31,7 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @@ -53,7 +53,7 @@ async def test_sensor_update_exception_handling( freezer: FrozenDateTimeFactory, ) -> None: """Test entity update data handles exceptions.""" - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry, [Platform.SENSOR]) expected_entity_id = "sensor.nanostation_5ac_ap_name_antenna_gain" signal_state = hass.states.get(expected_entity_id) @@ -65,7 +65,7 @@ async def test_sensor_update_exception_handling( mock_airos_client.login.side_effect = exception - freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds() + 1)) + freezer.tick(timedelta(seconds=SCAN_INTERVAL.total_seconds())) async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/airq/__init__.py b/tests/components/airq/__init__.py index 612761c0653..41bc1e467dc 100644 --- a/tests/components/airq/__init__.py +++ b/tests/components/airq/__init__.py @@ -1 +1,32 @@ """Tests for the air-Q integration.""" + +from unittest.mock import patch + +from homeassistant.components.airq.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .common import TEST_DEVICE_INFO, TEST_USER_DATA + +from tests.common import MockConfigEntry + + +async def setup_platform(hass: HomeAssistant, platform: Platform) -> None: + """Load AirQ integration. + + This function does not patch AirQ itself, rather it depends on being + run in presence of `mock_coordinator_airq` fixture, which patches calls + by `AirQCoordinator.airq`, which are done under `async_setup`. + + Patching airq.PLATFORMS allows to set up a single platform in isolation. + """ + config_entry = MockConfigEntry( + domain=DOMAIN, data=TEST_USER_DATA, unique_id=TEST_DEVICE_INFO["id"] + ) + config_entry.add_to_hass(hass) + + # The patching is now handled by the mock_airq fixture. + # We just need to load the component. + with patch("homeassistant.components.airq.PLATFORMS", [platform]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/airq/common.py b/tests/components/airq/common.py index 275da60e3a2..2e568c3c3cb 100644 --- a/tests/components/airq/common.py +++ b/tests/components/airq/common.py @@ -16,3 +16,4 @@ TEST_DEVICE_INFO = DeviceInfo( hw_version="hw", ) TEST_DEVICE_DATA = {"co2": 500.0, "Status": "OK"} +TEST_BRIGHTNESS = 42 diff --git a/tests/components/airq/conftest.py b/tests/components/airq/conftest.py index a132153a76f..21118c3ef27 100644 --- a/tests/components/airq/conftest.py +++ b/tests/components/airq/conftest.py @@ -5,6 +5,8 @@ from unittest.mock import AsyncMock, patch import pytest +from .common import TEST_BRIGHTNESS, TEST_DEVICE_DATA, TEST_DEVICE_INFO + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -13,3 +15,28 @@ def mock_setup_entry() -> Generator[AsyncMock]: "homeassistant.components.airq.async_setup_entry", return_value=True ) as mock_setup_entry: yield mock_setup_entry + + +@pytest.fixture +def mock_airq(): + """Mock the aioairq.AirQ object. + + The integration imports it in two places: in coordinator and config_flow. + """ + + with ( + patch( + "homeassistant.components.airq.coordinator.AirQ", + autospec=True, + ) as mock_airq_class, + patch( + "homeassistant.components.airq.config_flow.AirQ", + new=mock_airq_class, + ), + ): + airq = mock_airq_class.return_value + # Pre-configure default mock values for setup + airq.fetch_device_info = AsyncMock(return_value=TEST_DEVICE_INFO) + airq.get_latest_data = AsyncMock(return_value=TEST_DEVICE_DATA) + airq.get_current_brightness = AsyncMock(return_value=TEST_BRIGHTNESS) + yield airq diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py index 95c22cb12c8..66cacecdaaa 100644 --- a/tests/components/airq/test_config_flow.py +++ b/tests/components/airq/test_config_flow.py @@ -1,7 +1,7 @@ """Test the air-Q config flow.""" import logging -from unittest.mock import patch +from unittest.mock import AsyncMock from aioairq import InvalidAuth from aiohttp.client_exceptions import ClientConnectionError @@ -29,7 +29,11 @@ DEFAULT_OPTIONS = { } -async def test_form(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: +async def test_form( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_airq: AsyncMock, +) -> None: """Test we get the form.""" caplog.set_level(logging.DEBUG) result = await hass.config_entries.flow.async_init( @@ -38,53 +42,49 @@ async def test_form(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> No assert result["type"] is FlowResultType.FORM assert result["errors"] is None - with ( - patch("aioairq.AirQ.validate"), - patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - TEST_USER_DATA, - ) - await hass.async_block_till_done() - assert f"Creating an entry for {TEST_DEVICE_INFO['name']}" in caplog.text + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_USER_DATA, + ) + await hass.async_block_till_done() + assert f"Creating an entry for {TEST_DEVICE_INFO['name']}" in caplog.text assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DEVICE_INFO["name"] assert result2["data"] == TEST_USER_DATA -async def test_form_invalid_auth(hass: HomeAssistant) -> None: +async def test_form_invalid_auth(hass: HomeAssistant, mock_airq: AsyncMock) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioairq.AirQ.validate", side_effect=InvalidAuth): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_USER_DATA | {CONF_PASSWORD: "wrong_password"} - ) + mock_airq.validate.side_effect = InvalidAuth + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA | {CONF_PASSWORD: "wrong_password"} + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass: HomeAssistant) -> None: +async def test_form_cannot_connect(hass: HomeAssistant, mock_airq: AsyncMock) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioairq.AirQ.validate", side_effect=ClientConnectionError): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_USER_DATA - ) + mock_airq.validate.side_effect = ClientConnectionError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} -async def test_duplicate_error(hass: HomeAssistant) -> None: +async def test_duplicate_error(hass: HomeAssistant, mock_airq: AsyncMock) -> None: """Test that errors are shown when duplicates are added.""" MockConfigEntry( data=TEST_USER_DATA, @@ -96,13 +96,9 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with ( - patch("aioairq.AirQ.validate"), - patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_USER_DATA - ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA + ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/airq/test_coordinator.py b/tests/components/airq/test_coordinator.py index 6512d60ddbe..f45986df61d 100644 --- a/tests/components/airq/test_coordinator.py +++ b/tests/components/airq/test_coordinator.py @@ -1,7 +1,7 @@ """Test the air-Q coordinator.""" import logging -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest @@ -32,7 +32,9 @@ STATUS_WARMUP = { async def test_logging_in_coordinator_first_update_data( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_airq: AsyncMock, ) -> None: """Test that the first AirQCoordinator._async_update_data call logs necessary setup. @@ -48,11 +50,7 @@ async def test_logging_in_coordinator_first_update_data( assert "name" not in coordinator.device_info # First call: fetch missing device info - with ( - patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO), - patch("aioairq.AirQ.get_latest_data", return_value=TEST_DEVICE_DATA), - ): - await coordinator._async_update_data() + await coordinator._async_update_data() # check that the missing name is logged... assert ( @@ -71,7 +69,9 @@ async def test_logging_in_coordinator_first_update_data( async def test_logging_in_coordinator_subsequent_update_data( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_airq: AsyncMock, ) -> None: """Test that the second AirQCoordinator._async_update_data call has nothing to log. @@ -83,11 +83,7 @@ async def test_logging_in_coordinator_subsequent_update_data( coordinator = AirQCoordinator(hass, MOCKED_ENTRY) coordinator.device_info.update(DeviceInfo(**TEST_DEVICE_INFO)) - with ( - patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO), - patch("aioairq.AirQ.get_latest_data", return_value=TEST_DEVICE_DATA), - ): - await coordinator._async_update_data() + await coordinator._async_update_data() # check that the name _is not_ missing assert "name" in coordinator.device_info # and that nothing of the kind is logged @@ -102,19 +98,17 @@ async def test_logging_in_coordinator_subsequent_update_data( async def test_logging_when_warming_up_sensor_present( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_airq: AsyncMock, ) -> None: """Test that warming up sensors are logged.""" caplog.set_level(logging.DEBUG) coordinator = AirQCoordinator(hass, MOCKED_ENTRY) - with ( - patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO), - patch( - "aioairq.AirQ.get_latest_data", - return_value=TEST_DEVICE_DATA | {"Status": STATUS_WARMUP}, - ), - ): - await coordinator._async_update_data() + mock_airq.get_latest_data.return_value = TEST_DEVICE_DATA | { + "Status": STATUS_WARMUP + } + await coordinator._async_update_data() assert ( f"Following sensors are still warming up: {set(STATUS_WARMUP.keys())}" in caplog.text diff --git a/tests/components/airq/test_number.py b/tests/components/airq/test_number.py new file mode 100644 index 00000000000..b5fa4d65ef1 --- /dev/null +++ b/tests/components/airq/test_number.py @@ -0,0 +1,70 @@ +"""Test the NUMBER platform from air-Q integration.""" + +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from . import setup_platform +from .common import TEST_BRIGHTNESS, TEST_DEVICE_INFO + +ENTITY_ID = f"number.{TEST_DEVICE_INFO['name']}_led_brightness" + + +@pytest.fixture(autouse=True) +async def number_platform(hass: HomeAssistant, mock_airq: AsyncMock) -> None: + """Configure AirQ integration and validate the setup for NUMBER platform.""" + await setup_platform(hass, Platform.NUMBER) + + # Validate the setup + state = hass.states.get(ENTITY_ID) + assert state is not None, ( + f"{ENTITY_ID} not found among {hass.states.async_entity_ids()}" + ) + assert float(state.state) == TEST_BRIGHTNESS + + +@pytest.mark.parametrize("new_brightness", [0, 100, (TEST_BRIGHTNESS + 10) % 100]) +async def test_number_set_value( + hass: HomeAssistant, mock_airq: AsyncMock, new_brightness +) -> None: + """Test that setting value works.""" + # Simulate the device confirming the new brightness on the next poll + mock_airq.get_current_brightness.return_value = new_brightness + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": ENTITY_ID, "value": new_brightness}, + blocking=True, + ) + await hass.async_block_till_done() + + # Verify the API methods were called correctly + mock_airq.set_current_brightness.assert_called_once_with(new_brightness) + + # Validate that the update propagated to the state + state = hass.states.get(ENTITY_ID) + assert state is not None, ( + f"{ENTITY_ID} not found among {hass.states.async_entity_ids()}" + ) + assert float(state.state) == new_brightness + + +@pytest.mark.parametrize("new_brightness", [-1, 110]) +async def test_number_set_invalid_value_caught_by_hass( + hass: HomeAssistant, mock_airq: AsyncMock, new_brightness +) -> None: + """Test that setting incorrect values errors.""" + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + "number", + "set_value", + {"entity_id": ENTITY_ID, "value": new_brightness}, + blocking=True, + ) + + mock_airq.set_current_brightness.assert_not_called() diff --git a/tests/components/airthings/snapshots/test_sensor.ambr b/tests/components/airthings/snapshots/test_sensor.ambr index 67a210ca037..9cc3d1bcd13 100644 --- a/tests/components/airthings/snapshots/test_sensor.ambr +++ b/tests/components/airthings/snapshots/test_sensor.ambr @@ -263,7 +263,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '2960000001_pm1', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_device_types[view_plus][sensor.living_room_pm1-state] @@ -272,7 +272,7 @@ 'device_class': 'pm1', 'friendly_name': 'Living Room PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.living_room_pm1', @@ -319,7 +319,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '2960000001_pm25', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_device_types[view_plus][sensor.living_room_pm2_5-state] @@ -328,7 +328,7 @@ 'device_class': 'pm25', 'friendly_name': 'Living Room PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.living_room_pm2_5', diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index a7335017691..d52ee5733a1 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -245,7 +245,9 @@ async def test_get_action_capabilities( "arm_night": {"extra_fields": []}, "arm_vacation": {"extra_fields": []}, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } @@ -293,7 +295,9 @@ async def test_get_action_capabilities_legacy( "arm_night": {"extra_fields": []}, "arm_vacation": {"extra_fields": []}, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } @@ -338,19 +342,29 @@ async def test_get_action_capabilities_arm_code( expected_capabilities = { "arm_away": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_home": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_night": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_vacation": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } @@ -394,19 +408,29 @@ async def test_get_action_capabilities_arm_code_legacy( expected_capabilities = { "arm_away": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_home": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_night": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_vacation": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 3efacb80560..979bc33bb00 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -191,7 +191,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -226,7 +231,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index 22596706862..3c68b7b7626 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -8,9 +8,9 @@ from aioamazondevices.const import DEVICE_TYPE_TO_MODEL import pytest from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN -from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME +from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME from tests.common import MockConfigEntry @@ -80,7 +80,6 @@ def mock_config_entry() -> MockConfigEntry: domain=DOMAIN, title="Amazon Test Account", data={ - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: {"session": "test-session"}, diff --git a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr index 95798fca817..0f3c3647e90 100644 --- a/tests/components/alexa_devices/snapshots/test_diagnostics.ambr +++ b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr @@ -47,7 +47,6 @@ }), 'entry': dict({ 'data': dict({ - 'country': 'IT', 'login_data': dict({ 'session': 'test-session', }), diff --git a/tests/components/alexa_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py index e1b2974184b..9aea6fe4c44 100644 --- a/tests/components/alexa_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -6,17 +6,16 @@ from aioamazondevices.exceptions import ( CannotAuthenticate, CannotConnect, CannotRetrieveData, - WrongCountry, ) import pytest from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME +from .const import TEST_CODE, TEST_PASSWORD, TEST_USERNAME from tests.common import MockConfigEntry @@ -37,7 +36,6 @@ async def test_full_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -46,7 +44,6 @@ async def test_full_flow( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME assert result["data"] == { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_LOGIN_DATA: { @@ -63,7 +60,6 @@ async def test_full_flow( (CannotConnect, "cannot_connect"), (CannotAuthenticate, "invalid_auth"), (CannotRetrieveData, "cannot_retrieve_data"), - (WrongCountry, "wrong_country"), ], ) async def test_flow_errors( @@ -87,7 +83,6 @@ async def test_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -102,7 +97,6 @@ async def test_flow_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -131,7 +125,6 @@ async def test_already_configured( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_COUNTRY: TEST_COUNTRY, CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_CODE: TEST_CODE, @@ -215,3 +208,94 @@ async def test_reauth_not_successful( assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_PASSWORD] == "fake_password" assert mock_config_entry.data[CONF_CODE] == "111111" + + +async def test_reconfigure_successful( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the entry can be reconfigured.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # original entry + assert mock_config_entry.data[CONF_USERNAME] == TEST_USERNAME + + new_password = "new_fake_password" + + reconfigure_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: new_password, + CONF_CODE: TEST_CODE, + }, + ) + + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + + # changed entry + assert mock_config_entry.data[CONF_PASSWORD] == new_password + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (CannotRetrieveData, "cannot_retrieve_data"), + ], +) +async def test_reconfigure_fails( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: + """Test that the host can be reconfigured.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mock_amazon_devices_client.login_mode_interactive.side_effect = side_effect + + reconfigure_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: TEST_PASSWORD, + CONF_CODE: TEST_CODE, + }, + ) + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "reconfigure" + assert reconfigure_result["errors"] == {"base": error} + + mock_amazon_devices_client.login_mode_interactive.side_effect = None + + reconfigure_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: TEST_PASSWORD, + CONF_CODE: TEST_CODE, + }, + ) + + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + assert mock_config_entry.data == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_LOGIN_DATA: { + "customer_info": {"user_id": TEST_USERNAME}, + }, + } diff --git a/tests/components/alexa_devices/test_init.py b/tests/components/alexa_devices/test_init.py index 3100cfe5fa9..c628a5e00e7 100644 --- a/tests/components/alexa_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -4,12 +4,14 @@ from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion -from homeassistant.components.alexa_devices.const import DOMAIN +from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from . import setup_integration -from .const import TEST_SERIAL_NUMBER +from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME from tests.common import MockConfigEntry @@ -28,3 +30,32 @@ async def test_device_info( ) assert device_entry is not None assert device_entry == snapshot + + +async def test_migrate_entry( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful migration of entry data.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Amazon Test Account", + data={ + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_LOGIN_DATA: {"session": "test-session"}, + }, + unique_id=TEST_USERNAME, + version=1, + minor_version=0, + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.minor_version == 1 + assert config_entry.data["site"] == f"https://www.amazon.{TEST_COUNTRY}" diff --git a/tests/components/altruist/snapshots/test_sensor.ambr b/tests/components/altruist/snapshots/test_sensor.ambr index ca74e75542f..9340e10cbe8 100644 --- a/tests/components/altruist/snapshots/test_sensor.ambr +++ b/tests/components/altruist/snapshots/test_sensor.ambr @@ -319,7 +319,7 @@ 'supported_features': 0, 'translation_key': 'pm_10', 'unique_id': '5366960e8b18-SDS_P1', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[sensor.5366960e8b18_pm10-state] @@ -328,7 +328,7 @@ 'device_class': 'pm10', 'friendly_name': '5366960e8b18 PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.5366960e8b18_pm10', @@ -375,7 +375,7 @@ 'supported_features': 0, 'translation_key': 'pm_25', 'unique_id': '5366960e8b18-SDS_P2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[sensor.5366960e8b18_pm2_5-state] @@ -384,7 +384,7 @@ 'device_class': 'pm25', 'friendly_name': '5366960e8b18 PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.5366960e8b18_pm2_5', diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index 0e82d81f4e8..b4557fb2a4d 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -15,7 +15,11 @@ from amberelectric.models.spike_status import SpikeStatus from dateutil import parser import pytest -from homeassistant.components.amberelectric.const import CONF_SITE_ID, CONF_SITE_NAME +from homeassistant.components.amberelectric.const import ( + CONF_SITE_ID, + CONF_SITE_NAME, + REQUEST_TIMEOUT, +) from homeassistant.components.amberelectric.coordinator import AmberUpdateCoordinator from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant @@ -104,7 +108,9 @@ async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock) result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -136,7 +142,9 @@ async def test_fetch_no_general_site( await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) @@ -150,7 +158,9 @@ async def test_fetch_api_error(hass: HomeAssistant, current_price_api: Mock) -> result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_ONLY_SITE_ID, next=288 + GENERAL_ONLY_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -201,7 +211,9 @@ async def test_fetch_general_and_controlled_load_site( result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_AND_CONTROLLED_SITE_ID, next=288 + GENERAL_AND_CONTROLLED_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance @@ -241,7 +253,9 @@ async def test_fetch_general_and_feed_in_site( result = await data_service._async_update_data() current_price_api.get_current_prices.assert_called_with( - GENERAL_AND_FEED_IN_SITE_ID, next=288 + GENERAL_AND_FEED_IN_SITE_ID, + next=288, + _request_timeout=REQUEST_TIMEOUT, ) assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 0e14d556620..51579177e7e 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -975,6 +975,7 @@ async def test_submitting_legacy_integrations( assert snapshot == submitted_data +@pytest.mark.usefixtures("enable_custom_integrations") async def test_devices_payload( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -984,14 +985,17 @@ async def test_devices_payload( assert await async_setup_component(hass, "analytics", {}) assert await async_devices_payload(hass) == { "version": "home-assistant:1", - "no_model_id": [], + "home_assistant": MOCK_VERSION, "devices": [], } mock_config_entry = MockConfigEntry(domain="hue") mock_config_entry.add_to_hass(hass) - # Normal entry + mock_custom_config_entry = MockConfigEntry(domain="test") + mock_custom_config_entry.add_to_hass(hass) + + # Normal device with all fields device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={("device", "1")}, @@ -1005,7 +1009,7 @@ async def test_devices_payload( configuration_url="http://example.com/config", ) - # Ignored because service type + # Service type device device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={("device", "2")}, @@ -1014,7 +1018,7 @@ async def test_devices_payload( entry_type=dr.DeviceEntryType.SERVICE, ) - # Ignored because no model id + # Device without model_id no_model_id_config_entry = MockConfigEntry(domain="no_model_id") no_model_id_config_entry.add_to_hass(hass) device_registry.async_get_or_create( @@ -1023,14 +1027,14 @@ async def test_devices_payload( manufacturer="test-manufacturer", ) - # Ignored because no manufacturer + # Device without manufacturer device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={("device", "5")}, model_id="test-model-id", ) - # Entry with via device + # Device with via_device reference device_registry.async_get_or_create( config_entry_id=mock_config_entry.entry_id, identifiers={("device", "6")}, @@ -1039,9 +1043,17 @@ async def test_devices_payload( via_device=("device", "1"), ) + # Device from custom integration + device_registry.async_get_or_create( + config_entry_id=mock_custom_config_entry.entry_id, + identifiers={("device", "7")}, + manufacturer="test-manufacturer7", + model_id="test-model-id7", + ) + assert await async_devices_payload(hass) == { "version": "home-assistant:1", - "no_model_id": [], + "home_assistant": MOCK_VERSION, "devices": [ { "manufacturer": "test-manufacturer", @@ -1053,6 +1065,42 @@ async def test_devices_payload( "is_custom_integration": False, "has_configuration_url": True, "via_device": None, + "entry_type": None, + }, + { + "manufacturer": "test-manufacturer", + "model_id": "test-model-id", + "model": None, + "sw_version": None, + "hw_version": None, + "integration": "hue", + "is_custom_integration": False, + "has_configuration_url": False, + "via_device": None, + "entry_type": "service", + }, + { + "manufacturer": "test-manufacturer", + "model_id": None, + "model": None, + "sw_version": None, + "hw_version": None, + "integration": "no_model_id", + "has_configuration_url": False, + "via_device": None, + "entry_type": None, + }, + { + "manufacturer": None, + "model_id": "test-model-id", + "model": None, + "sw_version": None, + "hw_version": None, + "integration": "hue", + "is_custom_integration": False, + "has_configuration_url": False, + "via_device": None, + "entry_type": None, }, { "manufacturer": "test-manufacturer6", @@ -1064,6 +1112,20 @@ async def test_devices_payload( "is_custom_integration": False, "has_configuration_url": False, "via_device": 0, + "entry_type": None, + }, + { + "entry_type": None, + "has_configuration_url": False, + "hw_version": None, + "integration": "test", + "manufacturer": "test-manufacturer7", + "model": None, + "model_id": "test-model-id7", + "sw_version": None, + "via_device": None, + "is_custom_integration": True, + "custom_integration_version": "1.2.3", }, ], } diff --git a/tests/components/anthropic/snapshots/test_conversation.ambr b/tests/components/anthropic/snapshots/test_conversation.ambr index d97eaab41e4..8f7a3c43f5e 100644 --- a/tests/components/anthropic/snapshots/test_conversation.ambr +++ b/tests/components/anthropic/snapshots/test_conversation.ambr @@ -18,10 +18,29 @@ }), dict({ 'agent_id': 'conversation.claude_conversation', - 'content': 'Certainly, calling it now!', + 'content': None, + 'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'), 'role': 'assistant', + 'thinking_content': 'The user asked me to call a test function.Is it a test? What would the function do? Would it violate any privacy or security policies?', + 'tool_calls': None, + }), + dict({ + 'agent_id': 'conversation.claude_conversation', + 'content': None, + 'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'), + 'role': 'assistant', + 'thinking_content': None, + 'tool_calls': None, + }), + dict({ + 'agent_id': 'conversation.claude_conversation', + 'content': 'Certainly, calling it now!', + 'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'), + 'role': 'assistant', + 'thinking_content': "Okay, let's give it a shot. Will I pass the test?", 'tool_calls': list([ dict({ + 'external': False, 'id': 'toolu_0123456789AbCdEfGhIjKlM', 'tool_args': dict({ 'param1': 'test_value', @@ -40,7 +59,9 @@ dict({ 'agent_id': 'conversation.claude_conversation', 'content': 'I have successfully called the function', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) @@ -317,6 +338,39 @@ }), ]) # --- +# name: test_redacted_thinking + list([ + dict({ + 'attachments': None, + 'content': 'ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB', + 'role': 'user', + }), + dict({ + 'agent_id': 'conversation.claude_conversation', + 'content': None, + 'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'), + 'role': 'assistant', + 'thinking_content': None, + 'tool_calls': None, + }), + dict({ + 'agent_id': 'conversation.claude_conversation', + 'content': None, + 'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'), + 'role': 'assistant', + 'thinking_content': None, + 'tool_calls': None, + }), + dict({ + 'agent_id': 'conversation.claude_conversation', + 'content': 'How can I help you today?', + 'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'), + 'role': 'assistant', + 'thinking_content': None, + 'tool_calls': None, + }), + ]) +# --- # name: test_unknown_hass_api dict({ 'continue_conversation': False, diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index 83770e7ee34..f8cccd786fc 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -728,6 +728,7 @@ async def test_redacted_thinking( hass: HomeAssistant, mock_config_entry_with_extended_thinking: MockConfigEntry, mock_init_component, + snapshot: SnapshotAssertion, ) -> None: """Test extended thinking with redacted thinking blocks.""" with patch( @@ -756,8 +757,8 @@ async def test_redacted_thinking( chat_log = hass.data.get(conversation.chat_log.DATA_CHAT_LOGS).get( result.conversation_id ) - assert len(chat_log.content) == 3 - assert chat_log.content[2].content == "How can I help you today?" + # Don't test the prompt because it's not deterministic + assert chat_log.content[1:] == snapshot @patch("homeassistant.components.anthropic.entity.llm.AssistAPI._async_get_tools") diff --git a/tests/components/apcupsd/conftest.py b/tests/components/apcupsd/conftest.py new file mode 100644 index 00000000000..533694fdb1f --- /dev/null +++ b/tests/components/apcupsd/conftest.py @@ -0,0 +1,15 @@ +"""Common fixtures for the APC UPS Daemon (APCUPSD) tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.apcupsd.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py index e635b7d6681..0a61d8c0ddb 100644 --- a/tests/components/apcupsd/test_config_flow.py +++ b/tests/components/apcupsd/test_config_flow.py @@ -2,8 +2,8 @@ from __future__ import annotations -from copy import copy -from unittest.mock import patch +import asyncio +from unittest.mock import AsyncMock, patch import pytest @@ -18,19 +18,18 @@ from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS from tests.common import MockConfigEntry -def _patch_setup(): - return patch( - "homeassistant.components.apcupsd.async_setup_entry", - return_value=True, - ) - - -async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: - """Test config flow setup with connection error.""" +@pytest.mark.parametrize( + "exception", + [OSError(), asyncio.IncompleteReadError(partial=b"", expected=100), TimeoutError()], +) +async def test_config_flow_cannot_connect( + hass: HomeAssistant, exception: Exception +) -> None: + """Test config flow setup with a connection error.""" with patch( "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" - ) as mock_get: - mock_get.side_effect = OSError() + ) as mock_request_status: + mock_request_status.side_effect = exception result = await hass.config_entries.flow.async_init( DOMAIN, @@ -41,9 +40,11 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: assert result["errors"]["base"] == "cannot_connect" -async def test_config_flow_duplicate(hass: HomeAssistant) -> None: - """Test duplicate config flow setup.""" - # First add an exiting config entry to hass. +async def test_config_flow_duplicate_host_port( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test duplicate config flow setup with the same host / port.""" + # First add an existing config entry to hass. mock_entry = MockConfigEntry( version=1, domain=DOMAIN, @@ -54,44 +55,22 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: ) mock_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" - ) as mock_request_status, - _patch_setup(), - ): + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" + ) as mock_request_status: + # Assign the same host and port, which we should reject since the entry already exists. mock_request_status.return_value = MOCK_STATUS - - # Now, create the integration again using the same config data, we should reject - # the creation due same host / port. result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=CONF_DATA, + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - # Then, we create the integration once again using a different port. However, - # the apcaccess patch is kept to report the same serial number, we should - # reject the creation as well. - another_host = { - CONF_HOST: CONF_DATA[CONF_HOST], - CONF_PORT: CONF_DATA[CONF_PORT] + 1, + # Now we change the host with a different serial number and add it again. This should be successful. + another_host = CONF_DATA | {CONF_HOST: "another_host"} + mock_request_status.return_value = MOCK_STATUS | { + "SERIALNO": MOCK_STATUS["SERIALNO"] + "ZZZ" } - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=another_host, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - # Now we change the serial number and add it again. This should be successful. - another_device_status = copy(MOCK_STATUS) - another_device_status["SERIALNO"] = MOCK_STATUS["SERIALNO"] + "ZZZ" - mock_request_status.return_value = another_device_status - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -101,14 +80,52 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: assert result["data"] == another_host -async def test_flow_works(hass: HomeAssistant) -> None: +async def test_config_flow_duplicate_serial_number( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test duplicate config flow setup with different host but the same serial number.""" + # First add an existing config entry to hass. + mock_entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="APCUPSd", + data=CONF_DATA, + unique_id=MOCK_STATUS["SERIALNO"], + source=SOURCE_USER, + ) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" + ) as mock_request_status: + # Assign the different host and port, but we should still reject the creation since the + # serial number is the same as the existing entry. + mock_request_status.return_value = MOCK_STATUS + another_host = CONF_DATA | {CONF_HOST: "another_host"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=another_host, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Now we change the serial number and add it again. This should be successful. + mock_request_status.return_value = MOCK_STATUS | { + "SERIALNO": MOCK_STATUS["SERIALNO"] + "ZZZ" + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=another_host + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == another_host + + +async def test_flow_works(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test successful creation of config entries via user configuration.""" - with ( - patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", - return_value=MOCK_STATUS, - ), - _patch_setup() as mock_setup, + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", + return_value=MOCK_STATUS, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -123,8 +140,9 @@ async def test_flow_works(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_STATUS["UPSNAME"] assert result["data"] == CONF_DATA + assert result["result"].unique_id == MOCK_STATUS["SERIALNO"] - mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() @pytest.mark.parametrize( @@ -139,19 +157,19 @@ async def test_flow_works(hass: HomeAssistant) -> None: ], ) async def test_flow_minimal_status( - hass: HomeAssistant, extra_status: dict[str, str], expected_title: str + hass: HomeAssistant, + extra_status: dict[str, str], + expected_title: str, + mock_setup_entry: AsyncMock, ) -> None: """Test successful creation of config entries via user configuration when minimal status is reported. We test different combinations of minimal statuses, where the title of the integration will vary. """ - with ( - patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" - ) as mock_request_status, - _patch_setup() as mock_setup, - ): + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" + ) as mock_request_status: status = MOCK_MINIMAL_STATUS | extra_status mock_request_status.return_value = status @@ -162,10 +180,12 @@ async def test_flow_minimal_status( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA assert result["title"] == expected_title - mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() -async def test_reconfigure_flow_works(hass: HomeAssistant) -> None: +async def test_reconfigure_flow_works( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test successful reconfiguration of an existing entry.""" mock_entry = MockConfigEntry( version=1, @@ -184,18 +204,15 @@ async def test_reconfigure_flow_works(hass: HomeAssistant) -> None: # New configuration data with different host/port. new_conf_data = {CONF_HOST: "new_host", CONF_PORT: 4321} - with ( - patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", - return_value=MOCK_STATUS, - ), - _patch_setup() as mock_setup, + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", + return_value=MOCK_STATUS, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=new_conf_data ) await hass.async_block_till_done() - mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -205,8 +222,10 @@ async def test_reconfigure_flow_works(hass: HomeAssistant) -> None: assert mock_entry.data[CONF_PORT] == new_conf_data[CONF_PORT] -async def test_reconfigure_flow_cannot_connect(hass: HomeAssistant) -> None: - """Test reconfiguration with connection error.""" +async def test_reconfigure_flow_cannot_connect( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test reconfiguration with connection error and recovery.""" mock_entry = MockConfigEntry( version=1, domain=DOMAIN, @@ -234,6 +253,19 @@ async def test_reconfigure_flow_cannot_connect(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" + # Test recovery by fixing the connection issue. + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", + return_value=MOCK_STATUS, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=new_conf_data + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_entry.data == new_conf_data + @pytest.mark.parametrize( ("unique_id_before", "unique_id_after"), diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py index e7328603a59..4f6b55fe317 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -57,53 +57,6 @@ async def test_async_setup_entry( assert all(len(p.entities) > 0 for p in platforms) -async def test_multiple_integrations(hass: HomeAssistant) -> None: - """Test successful setup for multiple entries.""" - # Load two integrations from two mock hosts. - status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"} - status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"} - entries = ( - await async_init_integration( - hass, host="test1", status=status1, entry_id="entry-id-1" - ), - await async_init_integration( - hass, host="test2", status=status2, entry_id="entry-id-2" - ), - ) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert all(entry.state is ConfigEntryState.LOADED for entry in entries) - - # Since the two UPS device names are the same, we will have to add a "_2" suffix. - device_slug = slugify(MOCK_STATUS["UPSNAME"]) - state1 = hass.states.get(f"sensor.{device_slug}_load") - state2 = hass.states.get(f"sensor.{device_slug}_load_2") - assert state1 is not None and state2 is not None - assert state1.state != state2.state - - -async def test_multiple_integrations_different_devices(hass: HomeAssistant) -> None: - """Test successful setup for multiple entries with different device names.""" - status1 = MOCK_STATUS | {"SERIALNO": "XXXXX1", "UPSNAME": "MyUPS1"} - status2 = MOCK_STATUS | {"SERIALNO": "XXXXX2", "UPSNAME": "MyUPS2"} - entries = ( - await async_init_integration( - hass, host="test1", status=status1, entry_id="entry-id-1" - ), - await async_init_integration( - hass, host="test2", status=status2, entry_id="entry-id-2" - ), - ) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert all(entry.state is ConfigEntryState.LOADED for entry in entries) - - # The device names are different, so they are prefixed differently. - state1 = hass.states.get("sensor.myups1_load") - state2 = hass.states.get("sensor.myups2_load") - assert state1 is not None and state2 is not None - - @pytest.mark.parametrize( "error", [OSError(), asyncio.IncompleteReadError(partial=b"", expected=0)], @@ -127,34 +80,18 @@ async def test_connection_error(hass: HomeAssistant, error: Exception) -> None: async def test_unload_remove_entry(hass: HomeAssistant) -> None: """Test successful unload and removal of an entry.""" - # Load two integrations from two mock hosts. - entries = ( - await async_init_integration( - hass, host="test1", status=MOCK_STATUS, entry_id="entry-id-1" - ), - await async_init_integration( - hass, host="test2", status=MOCK_MINIMAL_STATUS, entry_id="entry-id-2" - ), + entry = await async_init_integration( + hass, host="test1", status=MOCK_STATUS, entry_id="entry-id-1" ) + assert entry.state is ConfigEntryState.LOADED - # Assert they are loaded. - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert all(entry.state is ConfigEntryState.LOADED for entry in entries) - - # Unload the first entry. - assert await hass.config_entries.async_unload(entries[0].entry_id) + # Unload the entry. + assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entries[0].state is ConfigEntryState.NOT_LOADED - assert entries[1].state is ConfigEntryState.LOADED + assert entry.state is ConfigEntryState.NOT_LOADED - # Unload the second entry. - assert await hass.config_entries.async_unload(entries[1].entry_id) - await hass.async_block_till_done() - assert all(entry.state is ConfigEntryState.NOT_LOADED for entry in entries) - - # Remove both entries. - for entry in entries: - await hass.config_entries.async_remove(entry.entry_id) + # Remove the entry. + await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 0 diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index 4da17b1c128..af163d3cbc1 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -59,7 +59,10 @@ async def test_state_update(hass: HomeAssistant) -> None: async def test_manual_update_entity(hass: HomeAssistant) -> None: - """Test manual update entity via service homeassistant/update_entity.""" + """Test multiple simultaneous manual update entity via service homeassistant/update_entity. + + We should only do network call once for the multiple simultaneous update entity services. + """ await async_init_integration(hass) device_slug = slugify(MOCK_STATUS["UPSNAME"]) @@ -103,37 +106,6 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: assert state.state == "15.0" -async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None: - """Test multiple simultaneous manual update entity via service homeassistant/update_entity. - - We should only do network call once for the multiple simultaneous update entity services. - """ - await async_init_integration(hass) - - device_slug = slugify(MOCK_STATUS["UPSNAME"]) - # Setup HASS for calling the update_entity service. - await async_setup_component(hass, "homeassistant", {}) - - with patch( - "aioapcaccess.request_status", return_value=MOCK_STATUS - ) as mock_request_status: - # Fast-forward time to just pass the initial debouncer cooldown. - future = utcnow() + timedelta(seconds=REQUEST_REFRESH_COOLDOWN) - async_fire_time_changed(hass, future) - await hass.services.async_call( - "homeassistant", - "update_entity", - { - ATTR_ENTITY_ID: [ - f"sensor.{device_slug}_load", - f"sensor.{device_slug}_input_voltage", - ] - }, - blocking=True, - ) - assert mock_request_status.call_count == 1 - - async def test_sensor_unknown(hass: HomeAssistant) -> None: """Test if our integration can properly mark certain sensors as unknown when it becomes so.""" await async_init_integration(hass, status=MOCK_MINIMAL_STATUS) diff --git a/tests/components/api/snapshots/test_init.ambr b/tests/components/api/snapshots/test_init.ambr new file mode 100644 index 00000000000..05b6bf31638 --- /dev/null +++ b/tests/components/api/snapshots/test_init.ambr @@ -0,0 +1,144 @@ +# serializer version: 1 +# name: test_api_get_services + list([ + dict({ + 'domain': 'group', + 'services': dict({ + 'reload': dict({ + 'description': 'Reloads group configuration, entities, and notify services from YAML-configuration.', + 'fields': dict({ + }), + 'name': 'Reload', + }), + 'remove': dict({ + 'description': 'Removes a group.', + 'fields': dict({ + 'object_id': dict({ + 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', + 'example': 'test_group', + 'name': 'Object ID', + 'required': True, + 'selector': dict({ + 'object': dict({ + }), + }), + }), + }), + 'name': 'Remove', + }), + 'set': dict({ + 'description': 'Creates/Updates a group.', + 'fields': dict({ + 'add_entities': dict({ + 'description': 'List of members to be added to the group. Cannot be used in combination with `Entities` or `Remove entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Add entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + 'all': dict({ + 'description': 'Enable this option if the group should only be used when all entities are in state `on`.', + 'name': 'All', + 'selector': dict({ + 'boolean': dict({ + }), + }), + }), + 'entities': dict({ + 'description': 'List of all members in the group. Cannot be used in combination with `Add entities` or `Remove entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + 'icon': dict({ + 'description': 'Name of the icon for the group.', + 'example': 'mdi:camera', + 'name': 'Icon', + 'selector': dict({ + 'icon': dict({ + }), + }), + }), + 'name': dict({ + 'description': 'Name of the group.', + 'example': 'My test group', + 'name': 'Name', + 'selector': dict({ + 'text': dict({ + }), + }), + }), + 'object_id': dict({ + 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', + 'example': 'test_group', + 'name': 'Object ID', + 'required': True, + 'selector': dict({ + 'text': dict({ + }), + }), + }), + 'remove_entities': dict({ + 'description': 'List of members to be removed from a group. Cannot be used in combination with `Entities` or `Add entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Remove entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + }), + 'name': 'Set', + }), + }), + }), + ]) +# --- +# name: test_api_get_services.1 + dict({ + 'domain': 'logger', + 'services': dict({ + 'set_default_level': dict({ + 'description': 'Translated description', + 'fields': dict({ + 'level': dict({ + 'description': 'Field description', + 'example': 'Field example', + 'name': 'Field name', + 'selector': dict({ + 'select': dict({ + 'options': list([ + 'debug', + 'info', + 'warning', + 'error', + 'fatal', + 'critical', + ]), + 'translation_key': 'level', + }), + }), + }), + }), + 'name': 'Translated name', + }), + 'set_level': dict({ + 'description': '', + 'fields': dict({ + }), + 'name': '', + }), + }), + }) +# --- diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index bc484a1632a..382b88b89ea 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -4,18 +4,24 @@ import asyncio from http import HTTPStatus import json from typing import Any -from unittest.mock import patch +from unittest.mock import ANY, patch from aiohttp import ServerDisconnectedError, web from aiohttp.test_utils import TestClient import pytest +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant import const, core as ha from homeassistant.auth.models import Credentials from homeassistant.bootstrap import DATA_LOGGING +from homeassistant.components.group import DOMAIN as DOMAIN_GROUP +from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER +from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH from homeassistant.core import HomeAssistant +from homeassistant.loader import Integration from homeassistant.setup import async_setup_component +from homeassistant.util.yaml.loader import JSON_TYPE from tests.common import CLIENT_ID, MockUser, async_mock_service from tests.typing import ClientSessionGenerator @@ -315,17 +321,72 @@ async def test_api_get_event_listeners( async def test_api_get_services( - hass: HomeAssistant, mock_api_client: TestClient + hass: HomeAssistant, + mock_api_client: TestClient, + snapshot: SnapshotAssertion, ) -> None: """Test if we can get a dict describing current services.""" + # Set up an integration that has services + assert await async_setup_component(hass, DOMAIN_GROUP, {DOMAIN_GROUP: {}}) + + # Set up an integration that has no services + assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) + resp = await mock_api_client.get(const.URL_API_SERVICES) data = await resp.json() - local_services = hass.services.async_services() - for serv_domain in data: - local = local_services.pop(serv_domain["domain"]) + assert data == snapshot - assert serv_domain["services"].keys() == local.keys() + # Set up an integration with legacy translations in services.yaml + def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE: + return { + "set_default_level": { + "description": "Translated description", + "fields": { + "level": { + "description": "Field description", + "example": "Field example", + "name": "Field name", + "selector": { + "select": { + "options": [ + "debug", + "info", + "warning", + "error", + "fatal", + "critical", + ], + "translation_key": "level", + } + }, + } + }, + "name": "Translated name", + }, + "set_level": None, + } + + await async_setup_component(hass, DOMAIN_LOGGER, {DOMAIN_LOGGER: {}}) + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.helpers.service._load_services_file", + side_effect=_load_services_file, + ), + patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={}, + ), + ): + resp = await mock_api_client.get(const.URL_API_SERVICES) + + data2 = await resp.json() + + assert data2 == [*data, {"domain": DOMAIN_LOGGER, "services": ANY}] + + assert data2[-1] == snapshot async def test_api_call_service_no_data( diff --git a/tests/components/arve/snapshots/test_sensor.ambr b/tests/components/arve/snapshots/test_sensor.ambr index eb51aa8c1f2..18643ac1755 100644 --- a/tests/components/arve/snapshots/test_sensor.ambr +++ b/tests/components/arve/snapshots/test_sensor.ambr @@ -144,7 +144,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_PM10', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[entry_pm2_5] @@ -181,7 +181,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_PM25', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[entry_temperature] @@ -314,7 +314,7 @@ 'device_class': 'pm10', 'friendly_name': 'Test Sensor PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_sensor_pm10', @@ -330,7 +330,7 @@ 'device_class': 'pm25', 'friendly_name': 'Test Sensor PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_sensor_pm2_5', diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index e20452a1f93..681f6e7759d 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant.components import stt, tts, wake_word +from homeassistant.components import conversation, stt, tts, wake_word from homeassistant.components.assist_pipeline import DOMAIN, select as assist_select from homeassistant.components.assist_pipeline.const import ( BYTES_PER_CHUNK, @@ -295,6 +295,11 @@ async def init_supporting_components( assert await async_setup_component(hass, tts.DOMAIN, {"tts": {"platform": "test"}}) assert await async_setup_component(hass, stt.DOMAIN, {"stt": {"platform": "test"}}) assert await async_setup_component(hass, "media_source", {}) + assert await async_setup_component(hass, "conversation", {"conversation": {}}) + + # Disable fuzzy matching by default for tests + agent = hass.data[conversation.DATA_DEFAULT_ENTITY] + agent.fuzzy_matching = False config_entry = MockConfigEntry(domain="test") config_entry.add_to_hass(hass) diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr index 95415ddb902..b6354b2342b 100644 --- a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -467,6 +467,7 @@ 'chat_log_delta': dict({ 'tool_calls': list([ dict({ + 'external': False, 'id': 'test_tool_id', 'tool_args': dict({ }), diff --git a/tests/components/asuswrt/common.py b/tests/components/asuswrt/common.py index d3953416281..541e74e5b39 100644 --- a/tests/components/asuswrt/common.py +++ b/tests/components/asuswrt/common.py @@ -1,7 +1,8 @@ """Test code shared between test files.""" +from unittest.mock import MagicMock + from aioasuswrt.asuswrt import Device as LegacyDevice -from pyasuswrt.asuswrt import Device as HttpDevice from homeassistant.components.asuswrt.const import ( CONF_SSH_KEY, @@ -59,8 +60,22 @@ MOCK_MACS = [ ] -def new_device(protocol, mac, ip, name): +def make_client(mac, ip, name, node): + """Create a modern mock client.""" + connection = MagicMock() + connection.ip_address = ip + connection.node = node + description = MagicMock() + description.name = name + description.mac = mac + client = MagicMock() + client.connection = connection + client.description = description + return client + + +def new_device(protocol, mac, ip, name, node=None): """Return a new device for specific protocol.""" if protocol in [PROTOCOL_HTTP, PROTOCOL_HTTPS]: - return HttpDevice(mac, ip, name, ROUTER_MAC_ADDR, None) + return make_client(mac, ip, name, node) return LegacyDevice(mac, ip, name) diff --git a/tests/components/asuswrt/conftest.py b/tests/components/asuswrt/conftest.py index f850a26b997..95c8f3dbf74 100644 --- a/tests/components/asuswrt/conftest.py +++ b/tests/components/asuswrt/conftest.py @@ -1,17 +1,25 @@ """Fixtures for Asuswrt component.""" +from datetime import datetime from unittest.mock import Mock, patch from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy from aioasuswrt.connection import TelnetConnection -from pyasuswrt.asuswrt import AsusWrtError, AsusWrtHttp +from asusrouter import AsusRouter, AsusRouterError +from asusrouter.modules.data import AsusData +from asusrouter.modules.identity import AsusDevice import pytest -from homeassistant.components.asuswrt.const import PROTOCOL_HTTP, PROTOCOL_SSH +from .common import ( + ASUSWRT_BASE, + MOCK_MACS, + PROTOCOL_HTTP, + PROTOCOL_SSH, + ROUTER_MAC_ADDR, + new_device, +) -from .common import ASUSWRT_BASE, MOCK_MACS, ROUTER_MAC_ADDR, new_device - -ASUSWRT_HTTP_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtHttp" +ASUSWRT_HTTP_LIB = f"{ASUSWRT_BASE}.bridge.AsusRouter" ASUSWRT_LEGACY_LIB = f"{ASUSWRT_BASE}.bridge.AsusWrtLegacy" MOCK_BYTES_TOTAL = 60000000000, 50000000000 @@ -29,8 +37,20 @@ MOCK_CPU_USAGE = { } MOCK_CURRENT_TRANSFER_RATES = 20000000, 10000000 MOCK_CURRENT_TRANSFER_RATES_HTTP = dict(enumerate(MOCK_CURRENT_TRANSFER_RATES)) +# Mock for AsusData.NETWORK return of both rates and total bytes +MOCK_CURRENT_NETWORK = { + "sensor_rx_rates": MOCK_CURRENT_TRANSFER_RATES[0] * 8, # AR works with bits + "sensor_tx_rates": MOCK_CURRENT_TRANSFER_RATES[1] * 8, # AR works with bits + "sensor_rx_bytes": MOCK_BYTES_TOTAL[0], + "sensor_tx_bytes": MOCK_BYTES_TOTAL[1], +} MOCK_LOAD_AVG_HTTP = {"load_avg_1": 1.1, "load_avg_5": 1.2, "load_avg_15": 1.3} MOCK_LOAD_AVG = list(MOCK_LOAD_AVG_HTTP.values()) +MOCK_SYSINFO = { + "sensor_load_avg1": MOCK_LOAD_AVG[0], + "sensor_load_avg5": MOCK_LOAD_AVG[1], + "sensor_load_avg15": MOCK_LOAD_AVG[2], +} MOCK_MEMORY_USAGE = { "mem_usage_perc": 52.4, "mem_total": 1048576, @@ -40,6 +60,10 @@ MOCK_MEMORY_USAGE = { MOCK_TEMPERATURES = {"2.4GHz": 40.2, "5.0GHz": 0, "CPU": 71.2} MOCK_TEMPERATURES_HTTP = {**MOCK_TEMPERATURES, "5.0GHz_2": 40.3, "6.0GHz": 40.4} MOCK_UPTIME = {"last_boot": "2024-08-02T00:47:00+00:00", "uptime": 1625927} +MOCK_BOOTTIME = { + "sensor_last_boot": datetime.fromisoformat(MOCK_UPTIME["last_boot"]), + "sensor_uptime": MOCK_UPTIME["uptime"], +} @pytest.fixture(name="patch_setup_entry") @@ -62,10 +86,14 @@ def mock_devices_legacy_fixture(): @pytest.fixture(name="mock_devices_http") def mock_devices_http_fixture(): - """Mock a list of devices.""" + """Mock a list of AsusRouter client devices for HTTP backend.""" return { - MOCK_MACS[0]: new_device(PROTOCOL_HTTP, MOCK_MACS[0], "192.168.1.2", "Test"), - MOCK_MACS[1]: new_device(PROTOCOL_HTTP, MOCK_MACS[1], "192.168.1.3", "TestTwo"), + MOCK_MACS[0]: new_device( + PROTOCOL_HTTP, MOCK_MACS[0], "192.168.1.2", "Test", "node1" + ), + MOCK_MACS[1]: new_device( + PROTOCOL_HTTP, MOCK_MACS[1], "192.168.1.3", "TestTwo", "node2" + ), } @@ -121,57 +149,90 @@ def mock_controller_connect_legacy_sens_fail(connect_legacy): @pytest.fixture(name="connect_http") def mock_controller_connect_http(mock_devices_http): """Mock a successful connection with http library.""" - with patch(ASUSWRT_HTTP_LIB, spec_set=AsusWrtHttp) as service_mock: - service_mock.return_value.is_connected = True - service_mock.return_value.mac = ROUTER_MAC_ADDR - service_mock.return_value.model = "FAKE_MODEL" - service_mock.return_value.firmware = "FAKE_FIRMWARE" - service_mock.return_value.async_get_connected_devices.return_value = ( - mock_devices_http + with patch(ASUSWRT_HTTP_LIB, spec_set=AsusRouter) as service_mock: + instance = service_mock.return_value + + # Simulate connection status + instance.connected = True + + # Identity + instance.async_get_identity.return_value = AsusDevice( + mac=ROUTER_MAC_ADDR, + model="FAKE_MODEL", + firmware="FAKE_FIRMWARE", ) - service_mock.return_value.async_get_traffic_bytes.return_value = ( - MOCK_BYTES_TOTAL_HTTP - ) - service_mock.return_value.async_get_traffic_rates.return_value = ( - MOCK_CURRENT_TRANSFER_RATES_HTTP - ) - service_mock.return_value.async_get_loadavg.return_value = MOCK_LOAD_AVG_HTTP - service_mock.return_value.async_get_temperatures.return_value = { - k: v for k, v in MOCK_TEMPERATURES_HTTP.items() if k != "5.0GHz" - } - service_mock.return_value.async_get_cpu_usage.return_value = MOCK_CPU_USAGE - service_mock.return_value.async_get_memory_usage.return_value = ( - MOCK_MEMORY_USAGE - ) - service_mock.return_value.async_get_uptime.return_value = MOCK_UPTIME + + # Data fetches via async_get_data + instance.async_get_data.side_effect = lambda datatype, *args, **kwargs: { + AsusData.CLIENTS: mock_devices_http, + AsusData.NETWORK: MOCK_CURRENT_NETWORK, + AsusData.SYSINFO: MOCK_SYSINFO, + AsusData.TEMPERATURE: { + k: v for k, v in MOCK_TEMPERATURES_HTTP.items() if k != "5.0GHz" + }, + AsusData.CPU: MOCK_CPU_USAGE, + AsusData.RAM: MOCK_MEMORY_USAGE, + AsusData.BOOTTIME: MOCK_BOOTTIME, + }[datatype] + yield service_mock +def make_async_get_data_side_effect(fail_types=None): + """Return a side effect for async_get_data that fails for specified AsusData types.""" + fail_types = set(fail_types or []) + + def side_effect(datatype, *args, **kwargs): + if datatype in fail_types: + raise AsusRouterError(f"{datatype} unavailable") + # Return valid mock data for other types + if datatype == AsusData.CLIENTS: + return {} + if datatype == AsusData.NETWORK: + return {} + if datatype == AsusData.SYSINFO: + return {} + if datatype == AsusData.TEMPERATURE: + return {} + if datatype == AsusData.CPU: + return {} + if datatype == AsusData.RAM: + return {} + if datatype == AsusData.BOOTTIME: + return {} + return {} + + return side_effect + + @pytest.fixture(name="connect_http_sens_fail") def mock_controller_connect_http_sens_fail(connect_http): - """Mock a successful connection using http library with sensors fail.""" - connect_http.return_value.mac = None - connect_http.return_value.async_get_connected_devices.side_effect = AsusWrtError - connect_http.return_value.async_get_traffic_bytes.side_effect = AsusWrtError - connect_http.return_value.async_get_traffic_rates.side_effect = AsusWrtError - connect_http.return_value.async_get_loadavg.side_effect = AsusWrtError - connect_http.return_value.async_get_temperatures.side_effect = AsusWrtError - connect_http.return_value.async_get_cpu_usage.side_effect = AsusWrtError - connect_http.return_value.async_get_memory_usage.side_effect = AsusWrtError - connect_http.return_value.async_get_uptime.side_effect = AsusWrtError + """Universal fixture to fail specified AsusData types.""" + + def _set_fail_types(fail_types): + connect_http.return_value.async_get_data.side_effect = ( + make_async_get_data_side_effect(fail_types) + ) + return connect_http + + return _set_fail_types @pytest.fixture(name="connect_http_sens_detect") def mock_controller_connect_http_sens_detect(): """Mock a successful sensor detection using http library.""" - with ( - patch( - f"{ASUSWRT_BASE}.bridge.AsusWrtHttpBridge._get_available_temperature_sensors", - return_value=[*MOCK_TEMPERATURES_HTTP], - ) as mock_sens_temp_detect, - patch( - f"{ASUSWRT_BASE}.bridge.AsusWrtHttpBridge._get_available_cpu_sensors", - return_value=[*MOCK_CPU_USAGE], - ) as mock_sens_cpu_detect, - ): - yield mock_sens_temp_detect, mock_sens_cpu_detect + + async def _get_sensors_side_effect(datatype): + if datatype == AsusData.TEMPERATURE: + return list(MOCK_TEMPERATURES_HTTP) + if datatype == AsusData.CPU: + return list(MOCK_CPU_USAGE) + if datatype == AsusData.SYSINFO: + return list(MOCK_SYSINFO) + return [] + + with patch( + f"{ASUSWRT_BASE}.bridge.AsusWrtHttpBridge._get_sensors", + side_effect=_get_sensors_side_effect, + ) as mock_sens_detect: + yield mock_sens_detect diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 83c3204d239..314bf030dbc 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -3,7 +3,8 @@ from socket import gaierror from unittest.mock import patch -from pyasuswrt import AsusWrtError +from asusrouter import AsusRouterError +from asusrouter.modules.identity import AsusDevice import pytest from homeassistant.components.asuswrt.const import ( @@ -128,7 +129,11 @@ async def test_user_http( assert flow_result["type"] is FlowResultType.FORM assert flow_result["step_id"] == "user" - connect_http.return_value.mac = unique_id + connect_http.return_value.async_get_identity.return_value = AsusDevice( + mac=unique_id, + model="FAKE_MODEL", + firmware="FAKE_FIRMWARE", + ) # test with all provided result = await hass.config_entries.flow.async_configure( @@ -297,7 +302,7 @@ async def test_on_connect_legacy_failed( @pytest.mark.parametrize( ("side_effect", "error"), [ - (AsusWrtError, "cannot_connect"), + (AsusRouterError, "cannot_connect"), (TypeError, "unknown"), (None, "cannot_connect"), ], @@ -311,7 +316,7 @@ async def test_on_connect_http_failed( context={"source": SOURCE_USER, "show_advanced_options": True}, ) - connect_http.return_value.is_connected = False + connect_http.return_value.connected = False connect_http.return_value.async_connect.side_effect = side_effect result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/asuswrt/test_helpers.py b/tests/components/asuswrt/test_helpers.py new file mode 100644 index 00000000000..6573ab9361c --- /dev/null +++ b/tests/components/asuswrt/test_helpers.py @@ -0,0 +1,95 @@ +"""Tests for AsusWRT helpers.""" + +from typing import Any + +import pytest + +from homeassistant.components.asuswrt.helpers import clean_dict, translate_to_legacy + +DICT_TO_CLEAN = { + "key1": "value1", + "key2": None, + "key3_state": "value3", + "key4_state": None, + "state": None, +} + +DICT_CLEAN = { + "key1": "value1", + "key3_state": "value3", + "key4_state": None, + "state": None, +} + +TRANSLATE_0_INPUT = { + "usage": "value1", + "cpu": "value2", +} + +TRANSLATE_0_OUTPUT = { + "mem_usage_perc": "value1", + "CPU": "value2", +} + +TRANSLATE_1_INPUT = { + "wan_rx": "value1", + "wan_rrx": "value2", +} + +TRANSLATE_1_OUTPUT = { + "sensor_rx_bytes": "value1", + "wan_rrx": "value2", +} + +TRANSLATE_2_INPUT = [ + "free", + "used", +] + +TRANSLATE_2_OUTPUT = [ + "mem_free", + "mem_used", +] + +TRANSLATE_3_INPUT = [ + "2ghz", + "2ghz2", +] + +TRANSLATE_3_OUTPUT = [ + "2.4GHz", + "2ghz2", +] + + +def test_clean_dict() -> None: + """Test clean_dict method.""" + + assert clean_dict(DICT_TO_CLEAN) == DICT_CLEAN + + +@pytest.mark.parametrize( + ("input", "expected"), + [ + # Case set 0: None as input -> None on output + (None, None), + # Case set 1: Dict structure should stay intact or translated + ({"key1": "value1", "key2": None}, {"key1": "value1", "key2": None}), + (TRANSLATE_0_INPUT, TRANSLATE_0_OUTPUT), + (TRANSLATE_1_INPUT, TRANSLATE_1_OUTPUT), + ({}, {}), + # Case set 2: List structure should stay intact or translated + (["key1", "key2"], ["key1", "key2"]), + (TRANSLATE_2_INPUT, TRANSLATE_2_OUTPUT), + (TRANSLATE_3_INPUT, TRANSLATE_3_OUTPUT), + ([], []), + # Case set 3: Anything else should be simply returned + (123, 123), + ("string", "string"), + (3.1415926535, 3.1415926535), + ], +) +def test_translate(input: Any, expected: Any) -> None: + """Test translate method.""" + + assert translate_to_legacy(input) == expected diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 929500f0bb7..c782605aab3 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -2,8 +2,9 @@ from datetime import timedelta +from asusrouter import AsusRouterError +from asusrouter.modules.data import AsusData from freezegun.api import FrozenDateTimeFactory -from pyasuswrt.exceptions import AsusWrtError, AsusWrtNotAvailableInfoError import pytest from homeassistant.components import device_tracker, sensor @@ -39,6 +40,7 @@ from .common import ( ROUTER_MAC_ADDR, new_device, ) +from .conftest import make_async_get_data_side_effect from tests.common import MockConfigEntry, async_fire_time_changed @@ -260,8 +262,8 @@ async def test_loadavg_sensors_unaivalable_http( config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_LOAD_AVG) config_entry.add_to_hass(hass) - connect_http.return_value.async_get_loadavg.side_effect = ( - AsusWrtNotAvailableInfoError + connect_http.return_value.async_get_data.side_effect = ( + make_async_get_data_side_effect([AsusData.SYSINFO]) ) # initial devices setup @@ -281,6 +283,7 @@ async def test_temperature_sensors_http_fail( hass: HomeAssistant, connect_http_sens_fail ) -> None: """Test fail creating AsusWRT temperature sensors.""" + _ = connect_http_sens_fail([AsusData.TEMPERATURE]) config_entry, sensor_prefix = _setup_entry( hass, CONFIG_DATA_HTTP, SENSORS_TEMPERATURES ) @@ -347,6 +350,7 @@ async def test_cpu_sensors_http_fail( hass: HomeAssistant, connect_http_sens_fail ) -> None: """Test fail creating AsusWRT cpu sensors.""" + _ = connect_http_sens_fail([AsusData.CPU]) config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU) config_entry.add_to_hass(hass) @@ -367,7 +371,10 @@ async def test_cpu_sensors_http_fail( async def test_cpu_sensors_http( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, connect_http + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + connect_http, + connect_http_sens_detect, ) -> None: """Test creating AsusWRT cpu sensors.""" config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU) @@ -461,7 +468,7 @@ async def test_connect_fail_legacy( @pytest.mark.parametrize( "side_effect", - [AsusWrtError, None], + [AsusRouterError, None], ) async def test_connect_fail_http( hass: HomeAssistant, connect_http, side_effect @@ -476,7 +483,7 @@ async def test_connect_fail_http( config_entry.add_to_hass(hass) connect_http.return_value.async_connect.side_effect = side_effect - connect_http.return_value.is_connected = False + connect_http.return_value.connected = False # initial setup fail await hass.config_entries.async_setup(config_entry.entry_id) @@ -524,6 +531,16 @@ async def test_sensors_polling_fails_http( connect_http_sens_detect, ) -> None: """Test AsusWRT sensors are unavailable when polling fails.""" + # Fail all relevant AsusData types for HTTP sensors + fail_types = [ + AsusData.NETWORK, + AsusData.CPU, + AsusData.SYSINFO, + AsusData.RAM, + AsusData.TEMPERATURE, + AsusData.BOOTTIME, + ] + _ = connect_http_sens_fail(fail_types) await _test_sensors_polling_fails(hass, freezer, CONFIG_DATA_HTTP, SENSORS_ALL_HTTP) diff --git a/tests/components/august/conftest.py b/tests/components/august/conftest.py index 78cb2cdad89..54f25a7a63b 100644 --- a/tests/components/august/conftest.py +++ b/tests/components/august/conftest.py @@ -5,6 +5,19 @@ from unittest.mock import patch import pytest from yalexs.manager.ratelimit import _RateLimitChecker +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.august.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + +USER_ID = "a76c25e5-49aa-4c14-cd0c-48a6931e2081" +CLIENT_ID = "1" + @pytest.fixture(name="mock_discovery", autouse=True) def mock_discovery_fixture(): @@ -20,3 +33,105 @@ def disable_ratelimit_checks_fixture(): """Disable rate limit checks.""" with patch.object(_RateLimitChecker, "register_wakeup"): yield + + +@pytest.fixture(name="mock_config_entry") +def mock_config_entry_fixture(jwt: str) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": "august", + "token": { + "access_token": jwt, + "scope": "any", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "user_id": "mock-user-id", + "expires_at": 1697753347, + }, + }, + unique_id=USER_ID, + ) + + +@pytest.fixture(name="mock_legacy_config_entry") +def mock_legacy_config_entry_fixture() -> MockConfigEntry: + """Return a legacy config entry without OAuth data.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + "login_method": "email", + "username": "my@email.tld", + "password": "test-password", + "install_id": None, + "timeout": 10, + "access_token_cache_file": ".my@email.tld.august.conf", + }, + unique_id="my@email.tld", + ) + + +@pytest.fixture(name="jwt") +def load_jwt_fixture() -> str: + """Load Fixture data.""" + return load_fixture("jwt", DOMAIN).strip("\n") + + +@pytest.fixture(name="legacy_jwt") +def load_legacy_jwt_fixture() -> str: + """Load legacy JWT fixture data.""" + return load_fixture("legacy_jwt", DOMAIN).strip("\n") + + +@pytest.fixture(name="reauth_jwt") +def load_reauth_jwt_fixture() -> str: + """Load reauth JWT fixture data.""" + return load_fixture("reauth_jwt", DOMAIN).strip("\n") + + +@pytest.fixture(name="migration_jwt") +def load_migration_jwt_fixture() -> str: + """Load migration JWT fixture data (has email for legacy migration).""" + return load_fixture("migration_jwt", DOMAIN).strip("\n") + + +@pytest.fixture(name="reauth_jwt_wrong_account") +def load_reauth_jwt_wrong_account_fixture() -> str: + """Load JWT fixture data for wrong account during reauth.""" + # Different userId, no email match + return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImRpZmZlcmVudC11c2VyLWlkIiwidkluc3RhbGxJZCI6ZmFsc2UsInZQYXNzd29yZCI6dHJ1ZSwidkVtYWlsIjp0cnVlLCJ2UGhvbmUiOnRydWUsImhhc0luc3RhbGxJZCI6ZmFsc2UsImhhc1Bhc3N3b3JkIjpmYWxzZSwiaGFzRW1haWwiOmZhbHNlLCJoYXNQaG9uZSI6ZmFsc2UsImlzTG9ja2VkT3V0IjpmYWxzZSwiY2FwdGNoYSI6IiIsImVtYWlsIjpbImRpZmZlcmVudEBlbWFpbC50bGQiXSwicGhvbmUiOltdLCJleHBpcmVzQXQiOiIyMDI0LTEyLTE4VDEzOjU0OjA1LjEzNFoiLCJ0ZW1wb3JhcnlBY2NvdW50Q3JlYXRpb25QYXNzd29yZExpbmsiOiIiLCJpYXQiOjE3MjQxNjIwNDUsImV4cCI6MTczNDUzMDA0NSwib2F1dGgiOnsiYXBwX25hbWUiOiJIb21lIEFzc2lzdGFudCIsImNsaWVudF9pZCI6ImIzY2QzZjBiLWZiOTctNGQ2Yy1iZWU5LWFmN2FiMDQ3NThjNyIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vYWNjb3VudC1saW5rLm5hYnVjYXNhLmNvbS9hdXRob3JpemVfY2FsbGJhY2siLCJwYXJ0bmVyX2lkIjoiNjU3OTc0ODgxMDY2Y2E0OGM5OWMwODI2In19.mK9nTAv7glYgtpLIkVF_dsrjrkRKYemdKfKMkgnafCU" + + +@pytest.fixture(name="client_credentials", autouse=True) +async def mock_client_credentials_fixture(hass: HomeAssistant) -> None: + """Mock client credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, "2"), + DOMAIN, + ) + + +@pytest.fixture(name="skip_cloud", autouse=True) +def skip_cloud_fixture(): + """Skip setting up cloud. + + Cloud already has its own tests for account link. + + We do not need to test it here as we only need to test our + usage of the oauth2 helpers. + """ + with patch("homeassistant.components.cloud.async_setup", return_value=True): + yield + + +@pytest.fixture +def mock_setup_entry(): + """Mock setup entry.""" + with patch( + "homeassistant.components.august.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/august/fixtures/jwt b/tests/components/august/fixtures/jwt new file mode 100644 index 00000000000..2e09f02fec2 --- /dev/null +++ b/tests/components/august/fixtures/jwt @@ -0,0 +1 @@ +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImE3NmMyNWU1LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6W10sInBob25lIjpbXSwiZXhwaXJlc0F0IjoiMjAyNC0xMi0xOFQxMzo1NDowNS4xMzRaIiwidGVtcG9yYXJ5QWNjb3VudENyZWF0aW9uUGFzc3dvcmRMaW5rIjoiIiwiaWF0IjoxNzI0MTYyMDQ1LCJleHAiOjE3MzQ1MzAwNDUsIm9hdXRoIjp7ImFwcF9uYW1lIjoiSG9tZSBBc3Npc3RhbnQiLCJjbGllbnRfaWQiOiJiM2NkM2YwYi1mYjk3LTRkNmMtYmVlOS1hZjdhYjA0NzU4YzciLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2FjY291bnQtbGluay5uYWJ1Y2FzYS5jb20vYXV0aG9yaXplX2NhbGxiYWNrIiwicGFydG5lcl9pZCI6IjY1Nzk3NDg4MTA2NmNhNDhjOTljMDgyNiJ9fQ.BdRo-dEr-osbDQGB2XzlI-mIj4gqULtapODt-sj-eA8 \ No newline at end of file diff --git a/tests/components/august/fixtures/legacy_jwt b/tests/components/august/fixtures/legacy_jwt new file mode 100644 index 00000000000..61c3d3db7b9 --- /dev/null +++ b/tests/components/august/fixtures/legacy_jwt @@ -0,0 +1 @@ +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImxlZ2FjeS11c2VyLWlkIiwidkluc3RhbGxJZCI6ZmFsc2UsInZQYXNzd29yZCI6dHJ1ZSwidkVtYWlsIjp0cnVlLCJ2UGhvbmUiOnRydWUsImhhc0luc3RhbGxJZCI6ZmFsc2UsImhhc1Bhc3N3b3JkIjpmYWxzZSwiaGFzRW1haWwiOmZhbHNlLCJoYXNQaG9uZSI6ZmFsc2UsImlzTG9ja2VkT3V0IjpmYWxzZSwiY2FwdGNoYSI6IiIsImVtYWlsIjpbIm15QGVtYWlsLnRsZCJdLCJwaG9uZSI6W10sImV4cGlyZXNBdCI6IjIwMjQtMTItMThUMTM6NTQ6MDUuMTM0WiIsInRlbXBvcmFyeUFjY291bnRDcmVhdGlvblBhc3N3b3JkTGluayI6IiIsImlhdCI6MTcyNDE2MjA0NSwiZXhwIjoxNzM0NTMwMDQ1LCJvYXV0aCI6eyJhcHBfbmFtZSI6IkhvbWUgQXNzaXN0YW50IiwiY2xpZW50X2lkIjoiYjNjZDNmMGItZmI5Ny00ZDZjLWJlZTktYWY3YWIwNDc1OGM3IiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6Ly9hY2NvdW50LWxpbmsubmFidWNhc2EuY29tL2F1dGhvcml6ZV9jYWxsYmFjayIsInBhcnRuZXJfaWQiOiI2NTc5NzQ4ODEwNjZjYTQ4Yzk5YzA4MjYifX0.BdRo-dEr-osbDQGB2XzlI-mIj4gqULtapODt-sj-eA8 \ No newline at end of file diff --git a/tests/components/august/fixtures/migration_jwt b/tests/components/august/fixtures/migration_jwt new file mode 100644 index 00000000000..3f96ef74f74 --- /dev/null +++ b/tests/components/august/fixtures/migration_jwt @@ -0,0 +1 @@ +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImE3NmMyNWU1LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZE91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6WyJteUBlbWFpbC50bGQiXSwicGhvbmUiOltdLCJleHBpcmVzQXQiOiIyMDI0LTEyLTE4VDEzOjU0OjA1LjEzNFoiLCJ0ZW1wb3JhcnlBY2NvdW50Q3JlYXRpb25QYXNzd29yZExpbmsiOiIiLCJpYXQiOjE3MjQxNjIwNDUsImV4cCI6MTczNDUzMDA0NSwib2F1dGgiOnsiYXBwX25hbWUiOiJIb21lIEFzc2lzdGFudCIsImNsaWVudF9pZCI6ImIzY2QzZjBiLWZiOTctNGQ2Yy1iZWU5LWFmN2FiMDQ3NThjNyIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vYWNjb3VudC1saW5rLm5hYnVjYXNhLmNvbS9hdXRob3JpemVfY2FsbGJhY2siLCJwYXJ0bmVyX2lkIjoiNjU3OTc0ODgxMDY2Y2E0OGM5OWMwODI2In19.migration-token \ No newline at end of file diff --git a/tests/components/august/fixtures/reauth_jwt b/tests/components/august/fixtures/reauth_jwt new file mode 100644 index 00000000000..ed6f355685a --- /dev/null +++ b/tests/components/august/fixtures/reauth_jwt @@ -0,0 +1 @@ +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpbnN0YWxsSWQiOiIiLCJyZWdpb24iOiJpcmVsYW5kLXByb2QtYXdzIiwiYXBwbGljYXRpb25JZCI6IiIsInVzZXJJZCI6ImE3NmMyNWU1LTQ5YWEtNGMxNC1jZDBjLTQ4YTY5MzFlMjA4MSIsInZJbnN0YWxsSWQiOmZhbHNlLCJ2UGFzc3dvcmQiOnRydWUsInZFbWFpbCI6dHJ1ZSwidlBob25lIjp0cnVlLCJoYXNJbnN0YWxsSWQiOmZhbHNlLCJoYXNQYXNzd29yZCI6ZmFsc2UsImhhc0VtYWlsIjpmYWxzZSwiaGFzUGhvbmUiOmZhbHNlLCJpc0xvY2tlZG91dCI6ZmFsc2UsImNhcHRjaGEiOiIiLCJlbWFpbCI6WyJteUBlbWFpbC50bGQiXSwicGhvbmUiOltdLCJleHBpcmVzQXQiOiIyMDI0LTEyLTE4VDEzOjU0OjA1LjEzNFoiLCJ0ZW1wb3JhcnlBY2NvdW50Q3JlYXRpb25QYXNzd29yZExpbmsiOiIiLCJpYXQiOjE3MjQxNjIwNDUsImV4cCI6MTczNDUzMDA0NSwib2F1dGgiOnsiYXBwX25hbWUiOiJIb21lIEFzc2lzdGFudCIsImNsaWVudF9pZCI6ImIzY2QzZjBiLWZiOTctNGQ2Yy1iZWU5LWFmN2FiMDQ3NThjNyIsInJlZGlyZWN0X3VyaSI6Imh0dHBzOi8vYWNjb3VudC1saW5rLm5hYnVjYXNhLmNvbS9hdXRob3JpemVfY2FsbGJhY2siLCJwYXJ0bmVyX2lkIjoiNjU3OTc0ODgxMDY2Y2E0OGM5OWMwODI2In19.reauth-updated-token \ No newline at end of file diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 43cc4957445..3201226afc5 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Iterable +from contextlib import contextmanager import json import os import time @@ -26,98 +27,184 @@ from yalexs.activity import ( DoorOperationActivity, LockOperationActivity, ) -from yalexs.authenticator_common import AuthenticationState +from yalexs.api_async import ApiAsync +from yalexs.authenticator_common import Authentication, AuthenticationState from yalexs.const import Brand from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.lock import Lock, LockDetail +from yalexs.manager.ratelimit import _RateLimitChecker from yalexs.pubnub_async import AugustPubNub -from homeassistant.components.august.const import CONF_BRAND, CONF_LOGIN_METHOD, DOMAIN +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.august.const import DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +USER_ID = "a76c25e5-49aa-4c14-cd0c-48a6931e2081" -def _mock_get_config(brand: Brand = Brand.AUGUST): + +def _mock_get_config( + brand: Brand = Brand.YALE_AUGUST, jwt: str | None = None +) -> dict[str, Any]: """Return a default august config.""" return { DOMAIN: { - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "mocked_username", - CONF_PASSWORD: "mocked_password", - CONF_BRAND: brand, + "auth_implementation": "august", + "token": { + "access_token": jwt or "access_token", + "expires_in": 1, + "refresh_token": "refresh_token", + "expires_at": time.time() + 3600, + "service": "august", + }, } } -def _mock_authenticator(auth_state): +def _mock_authenticator(auth_state: AuthenticationState) -> Authentication: """Mock an august authenticator.""" authenticator = MagicMock() type(authenticator).state = PropertyMock(return_value=auth_state) return authenticator -def _timetoken(): +def _timetoken() -> str: return str(time.time_ns())[:-2] -@patch("yalexs.manager.gateway.ApiAsync") -@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate") -async def _mock_setup_august( - hass: HomeAssistant, api_instance, pubnub_mock, authenticate_mock, api_mock, brand +async def mock_august_config_entry( + hass: HomeAssistant, ) -> MockConfigEntry: - """Set up august integration.""" - authenticate_mock.side_effect = MagicMock( - return_value=_mock_august_authentication( - "original_token", 1234, AuthenticationState.AUTHENTICATED - ) - ) - api_mock.return_value = api_instance - entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config(brand)[DOMAIN], - options={}, - ) + """Mock august config entry and client credentials.""" + entry = mock_config_entry() entry.add_to_hass(hass) + return entry + + +def mock_config_entry(jwt: str | None = None) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config(jwt=jwt)[DOMAIN], + options={}, + unique_id=USER_ID, + ) + + +async def mock_client_credentials(hass: HomeAssistant) -> ClientCredential: + """Mock client credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential("1", "2"), + DOMAIN, + ) + + +@contextmanager +def patch_august_setup(): + """Patch august setup process.""" with ( + patch("yalexs.manager.gateway.ApiAsync") as api_mock, + patch.object(_RateLimitChecker, "register_wakeup") as authenticate_mock, + patch( + "homeassistant.components.august.config_entry_oauth2_flow.async_get_config_entry_implementation" + ), + ): + yield api_mock, authenticate_mock + + +async def _mock_setup_august( + hass: HomeAssistant, + api_instance: ApiAsync, + pubnub_mock: AugustPubNub, + authenticate_side_effect: MagicMock, +) -> ConfigEntry: + """Set up august integration.""" + entry = await mock_august_config_entry(hass) + with ( + patch_august_setup() as patched_setup, patch.object(pubnub_mock, "run"), patch("yalexs.manager.data.AugustPubNub", return_value=pubnub_mock), ): - assert await hass.config_entries.async_setup(entry.entry_id) + api_mock, authenticate_mock = patched_setup + authenticate_mock.side_effect = authenticate_side_effect + api_mock.return_value = api_instance + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() return entry async def _create_august_with_devices( hass: HomeAssistant, - devices: Iterable[LockDetail | DoorbellDetail], + devices: Iterable[LockDetail | DoorbellDetail] | None = None, api_call_side_effects: dict[str, Any] | None = None, activities: list[Any] | None = None, pubnub: AugustPubNub | None = None, - brand: Brand = Brand.AUGUST, -) -> ConfigEntry: - entry, _ = await _create_august_api_with_devices( - hass, devices, api_call_side_effects, activities, pubnub, brand + brand: Brand = Brand.YALE_AUGUST, + authenticate_side_effect: MagicMock | None = None, +) -> tuple[ConfigEntry, AugustPubNub]: + entry, _, pubnub_instance = await _create_august_api_with_devices( + hass, + devices, + api_call_side_effects, + activities, + pubnub, + brand, + authenticate_side_effect, ) - return entry + return entry, pubnub_instance async def _create_august_api_with_devices( hass: HomeAssistant, - devices: Iterable[LockDetail | DoorbellDetail], + devices: Iterable[LockDetail | DoorbellDetail] | None = None, api_call_side_effects: dict[str, Any] | None = None, - activities: list[Any] | None = None, + activities: dict[str, Any] | None = None, pubnub: AugustPubNub | None = None, - brand: Brand = Brand.AUGUST, -) -> tuple[MockConfigEntry, MagicMock]: + brand: Brand = Brand.YALE_AUGUST, + authenticate_side_effect: MagicMock | None = None, +) -> tuple[ConfigEntry, ApiAsync, AugustPubNub]: if api_call_side_effects is None: api_call_side_effects = {} + if devices is None: + devices = () + + update_api_call_side_effects(api_call_side_effects, devices, activities) + + api_instance = await make_mock_api(api_call_side_effects, brand) + if pubnub is None: pubnub = AugustPubNub() + + pubnub.run = AsyncMock() + + entry = await _mock_setup_august( + hass, + api_instance, + pubnub, + authenticate_side_effect=authenticate_side_effect, + ) + + return entry, api_instance, pubnub + + +def update_api_call_side_effects( + api_call_side_effects: dict[str, Any], + devices: Iterable[LockDetail | DoorbellDetail], + activities: dict[str, Any] | None = None, +) -> None: + """Update side effects dict from devices and activities.""" + device_data = {"doorbells": [], "locks": []} - for device in devices: + for device in devices or (): if isinstance(device, LockDetail): device_data["locks"].append( {"base": _mock_august_lock(device.device_id), "detail": device} @@ -127,7 +214,7 @@ async def _create_august_api_with_devices( { "base": _mock_august_doorbell( deviceid=device.device_id, - brand=device._data.get("brand", Brand.AUGUST), + brand=device._data.get("brand", Brand.YALE_AUGUST), ), "detail": device, } @@ -200,24 +287,12 @@ async def _create_august_api_with_devices( "async_unlatch_return_activities", unlock_return_activities_side_effect ) - api_instance, entry = await _mock_setup_august_with_api_side_effects( - hass, api_call_side_effects, pubnub, brand - ) - if device_data["locks"]: - # Ensure we sync status when the integration is loaded if there - # are any locks - assert api_instance.async_status_async.mock_calls - - return entry, api_instance - - -async def _mock_setup_august_with_api_side_effects( - hass: HomeAssistant, +async def make_mock_api( api_call_side_effects: dict[str, Any], - pubnub: AugustPubNub, - brand: Brand = Brand.AUGUST, -): + brand: Brand = Brand.YALE_AUGUST, +) -> ApiAsync: + """Make a mock ApiAsync instance.""" api_instance = MagicMock(name="Api", brand=brand) if api_call_side_effects["get_lock_detail"]: @@ -267,12 +342,12 @@ async def _mock_setup_august_with_api_side_effects( api_instance.async_unlatch_async = AsyncMock() api_instance.async_unlatch = AsyncMock() - return api_instance, await _mock_setup_august( - hass, api_instance, pubnub, brand=brand - ) + return api_instance -def _mock_august_authentication(token_text, token_timestamp, state): +def _mock_august_authentication( + token_text: str, token_timestamp: float, state: AuthenticationState +) -> Authentication: authentication = MagicMock(name="yalexs.authentication") type(authentication).state = PropertyMock(return_value=state) type(authentication).access_token = PropertyMock(return_value=token_text) @@ -282,13 +357,15 @@ def _mock_august_authentication(token_text, token_timestamp, state): return authentication -def _mock_august_lock(lockid="mocklockid1", houseid="mockhouseid1"): +def _mock_august_lock( + lockid: str = "mocklockid1", houseid: str = "mockhouseid1" +) -> Lock: return Lock(lockid, _mock_august_lock_data(lockid=lockid, houseid=houseid)) def _mock_august_doorbell( - deviceid="mockdeviceid1", houseid="mockhouseid1", brand=Brand.AUGUST -): + deviceid="mockdeviceid1", houseid="mockhouseid1", brand=Brand.YALE_AUGUST +) -> Doorbell: return Doorbell( deviceid, _mock_august_doorbell_data(deviceid=deviceid, houseid=houseid, brand=brand), @@ -296,8 +373,10 @@ def _mock_august_doorbell( def _mock_august_doorbell_data( - deviceid="mockdeviceid1", houseid="mockhouseid1", brand=Brand.AUGUST -): + deviceid: str = "mockdeviceid1", + houseid: str = "mockhouseid1", + brand: Brand = Brand.YALE_AUGUST, +) -> dict[str, Any]: return { "_id": deviceid, "DeviceID": deviceid, @@ -317,7 +396,9 @@ def _mock_august_doorbell_data( } -def _mock_august_lock_data(lockid="mocklockid1", houseid="mockhouseid1"): +def _mock_august_lock_data( + lockid: str = "mocklockid1", houseid: str = "mockhouseid1" +) -> dict[str, Any]: return { "_id": lockid, "LockID": lockid, @@ -366,12 +447,12 @@ async def _mock_lock_from_fixture(hass: HomeAssistant, path: str) -> LockDetail: return LockDetail(json_dict) -async def _mock_doorbell_from_fixture(hass: HomeAssistant, path: str) -> DoorbellDetail: +async def _mock_doorbell_from_fixture(hass: HomeAssistant, path: str) -> LockDetail: json_dict = await _load_json_fixture(hass, path) return DoorbellDetail(json_dict) -async def _load_json_fixture(hass: HomeAssistant, path: str) -> Any: +async def _load_json_fixture(hass: HomeAssistant, path: str) -> dict[str, Any]: fixture = await hass.async_add_executor_job( load_fixture, os.path.join("august", path) ) @@ -390,7 +471,9 @@ async def _mock_lock_with_unlatch(hass: HomeAssistant) -> LockDetail: return await _mock_lock_from_fixture(hass, "get_lock.online_with_unlatch.json") -def _mock_lock_operation_activity(lock, action, offset): +def _mock_lock_operation_activity( + lock: Lock, action: str, offset: float +) -> LockOperationActivity: return LockOperationActivity( SOURCE_LOCK_OPERATE, { @@ -402,7 +485,9 @@ def _mock_lock_operation_activity(lock, action, offset): ) -def _mock_door_operation_activity(lock, action, offset): +def _mock_door_operation_activity( + lock: Lock, action: str, offset: float +) -> DoorOperationActivity: return DoorOperationActivity( SOURCE_LOCK_OPERATE, { diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 563221635f8..3f88316b990 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -249,7 +249,7 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: pubnub = AugustPubNub() activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json") - config_entry = await _create_august_with_devices( + config_entry, _ = await _create_august_with_devices( hass, [lock_one], activities=activities, pubnub=pubnub ) states = hass.states diff --git a/tests/components/august/test_button.py b/tests/components/august/test_button.py index 948b59b2286..9cbbee03b12 100644 --- a/tests/components/august/test_button.py +++ b/tests/components/august/test_button.py @@ -12,7 +12,7 @@ async def test_wake_lock(hass: HomeAssistant) -> None: lock_one = await _mock_lock_from_fixture( hass, "get_lock.online_with_doorsense.json" ) - _, api_instance = await _create_august_api_with_devices(hass, [lock_one]) + _, api_instance, _ = await _create_august_api_with_devices(hass, [lock_one]) entity_id = "button.online_with_doorsense_name_wake" binary_sensor_online_with_doorsense_name = hass.states.get(entity_id) assert binary_sensor_online_with_doorsense_name is not None diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index b3138342b8c..9f06b20f529 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -1,399 +1,456 @@ """Test the August config flow.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import ANY, Mock, patch -from yalexs.authenticator_common import ValidationResult -from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation +import pytest -from homeassistant.components.august.const import ( - CONF_ACCESS_TOKEN_CACHE_FILE, - CONF_BRAND, - CONF_INSTALL_ID, - CONF_LOGIN_METHOD, - DOMAIN, - VERIFICATION_CODE_KEY, +from homeassistant.components.august.application_credentials import ( + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, ) +from homeassistant.components.august.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + +CLIENT_ID = "1" +USER_ID = "a76c25e5-49aa-4c14-cd0c-48a6931e2081" -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" +@pytest.fixture +def mock_setup_entry() -> Generator[Mock]: + """Patch setup entry.""" + with patch( + "homeassistant.components.august.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures("mock_setup_entry") +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + jwt: str, +) -> None: + """Check full flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), - patch( - "homeassistant.components.august.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_BRAND: "august", - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": jwt, + "scope": "any", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "user_id": "mock-user-id", + "expires_at": 1697753347, + }, + ) + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.unique_id == USER_ID assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "my@email.tld" - assert result2["data"] == { - CONF_BRAND: "august", - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_INSTALL_ID: None, - CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", + assert result2["result"].unique_id == USER_ID + assert entry.data == { + "auth_implementation": "august", + "token": { + "access_token": jwt, + "expires_at": ANY, + "expires_in": ANY, + "refresh_token": "mock-refresh-token", + "scope": "any", + "user_id": "mock-user-id", + }, } - assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth.""" +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures("mock_setup_entry") +async def test_full_flow_already_exists( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + jwt: str, + mock_config_entry: MockConfigEntry, +) -> None: + """Check full flow for a user that already exists.""" + + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=InvalidAuth, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_BRAND: "august", - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_user_unexpected_exception(hass: HomeAssistant) -> None: - """Test we handle an unexpected exception.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=ValueError("something exploded"), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_BRAND: "august", - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "unhandled"} - assert result2["description_placeholders"] == {"error": "something exploded"} - - -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=CannotConnect, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_needs_validate(hass: HomeAssistant) -> None: - """Test we present validation when we need to validate.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=RequireValidation, - ), - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - }, - ) - - assert len(mock_send_verification_code.mock_calls) == 1 - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] is None - assert result2["step_id"] == "validation" - - # Try with the WRONG verification code give us the form back again - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=RequireValidation, - ), - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", - return_value=ValidationResult.INVALID_VERIFICATION_CODE, - ) as mock_validate_verification_code, - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code, - ): - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {VERIFICATION_CODE_KEY: "incorrect"}, - ) - - # Make sure we do not resend the code again - # so they have a chance to retry - assert len(mock_send_verification_code.mock_calls) == 0 - assert len(mock_validate_verification_code.mock_calls) == 1 - assert result3["type"] is FlowResultType.FORM - assert result3["errors"] == {"base": "invalid_verification_code"} - assert result3["step_id"] == "validation" - - # Try with the CORRECT verification code and we setup - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", - return_value=ValidationResult.VALIDATED, - ) as mock_validate_verification_code, - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code, - patch( - "homeassistant.components.august.async_setup_entry", return_value=True - ) as mock_setup_entry, - ): - result4 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {VERIFICATION_CODE_KEY: "correct"}, - ) - await hass.async_block_till_done() - - assert len(mock_send_verification_code.mock_calls) == 0 - assert len(mock_validate_verification_code.mock_calls) == 1 - assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["title"] == "my@email.tld" - assert result4["data"] == { - CONF_BRAND: "august", - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_INSTALL_ID: None, - CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_reauth(hass: HomeAssistant) -> None: - """Test reauthenticate.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - CONF_INSTALL_ID: None, - CONF_TIMEOUT: 10, - CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", }, - unique_id="my@email.tld", ) - entry.add_to_hass(hass) - result = await entry.start_reauth_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), - patch( - "homeassistant.components.august.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_PASSWORD: "new-test-password", - }, - ) - await hass.async_block_till_done() + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + aioclient_mock.clear_requests() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": jwt, + "scope": "any", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "user_id": "mock-user-id", + "expires_at": 1697753347, + }, + ) + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert len(mock_setup_entry.mock_calls) == 1 + assert result2["reason"] == "already_configured" -async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None: - """Test reauthenticate with 2fa.""" +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, + reauth_jwt: str, +) -> None: + """Test the reauthentication case updates the existing config entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - CONF_INSTALL_ID: None, - CONF_TIMEOUT: 10, - CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", + mock_config_entry.add_to_hass(hass) + + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "auth" + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", }, - unique_id="my@email.tld", ) - entry.add_to_hass(hass) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" - result = await entry.start_reauth_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=RequireValidation, - ), - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_PASSWORD: "new-test-password", - }, - ) - await hass.async_block_till_done() - - assert len(mock_send_verification_code.mock_calls) == 1 - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] is None - assert result2["step_id"] == "validation" - - # Try with the CORRECT verification code and we setup - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_validate_verification_code", - return_value=ValidationResult.VALIDATED, - ) as mock_validate_verification_code, - patch( - "yalexs.manager.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code, - patch( - "homeassistant.components.august.async_setup_entry", return_value=True - ) as mock_setup_entry, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {VERIFICATION_CODE_KEY: "correct"}, - ) - await hass.async_block_till_done() - - assert len(mock_validate_verification_code.mock_calls) == 1 - assert len(mock_send_verification_code.mock_calls) == 0 - assert result3["type"] is FlowResultType.ABORT - assert result3["reason"] == "reauth_successful" - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_switching_brands(hass: HomeAssistant) -> None: - """Test brands can be switched by setting up again.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - CONF_INSTALL_ID: None, - CONF_TIMEOUT: 10, - CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf", + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": reauth_jwt, + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "user_id": USER_ID, + "token_type": "Bearer", + "expires_at": 1697753347, }, - unique_id="my@email.tld", ) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.unique_id == USER_ID + assert "token" in mock_config_entry.data + # Verify access token is refreshed + assert mock_config_entry.data["token"]["access_token"] == reauth_jwt + + +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reauth_wrong_account( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, + reauth_jwt_wrong_account: str, + jwt: str, +) -> None: + """Test the reauthentication aborts, if user tries to reauthenticate with another account.""" + assert mock_config_entry.data["token"]["access_token"] == jwt + + mock_config_entry.add_to_hass(hass) + + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "auth" + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" - with ( - patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), - patch( - "homeassistant.components.august.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_BRAND: "yale_access", - CONF_LOGIN_METHOD: "email", - CONF_USERNAME: "my@email.tld", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": reauth_jwt_wrong_account, + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert len(mock_setup_entry.mock_calls) == 1 - assert entry.data[CONF_BRAND] == "yale_access" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_invalid_user" + + assert mock_config_entry.unique_id == USER_ID + assert "token" in mock_config_entry.data + # Verify access token is like before + assert mock_config_entry.data["token"]["access_token"] == jwt + + +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures("mock_setup_entry") +async def test_legacy_migration_with_email_match( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_legacy_config_entry: MockConfigEntry, + migration_jwt: str, +) -> None: + """Test migration from legacy username/password config to OAuth with email validation.""" + + mock_legacy_config_entry.add_to_hass(hass) + + # Start reauth flow (triggered by ConfigEntryAuthFailed in async_setup_entry) + mock_legacy_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "auth" + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": migration_jwt, # JWT with email: ["my@email.tld"] + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "user_id": USER_ID, + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + # Verify the entry was updated with new unique_id and OAuth data + assert mock_legacy_config_entry.unique_id == USER_ID # Updated from email to userId + assert "token" in mock_legacy_config_entry.data + assert mock_legacy_config_entry.data["auth_implementation"] == "august" + assert mock_legacy_config_entry.data["token"]["access_token"] == migration_jwt + + +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures("mock_setup_entry") +async def test_legacy_migration_wrong_email( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_legacy_config_entry: MockConfigEntry, + reauth_jwt_wrong_account: str, +) -> None: + """Test migration from legacy config fails when email doesn't match.""" + + mock_legacy_config_entry.add_to_hass(hass) + + # Start reauth flow + mock_legacy_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "auth" + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": reauth_jwt_wrong_account, # JWT with email: ["different@email.tld"] + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_invalid_user" + + # Verify the entry was NOT updated + assert mock_legacy_config_entry.unique_id == "my@email.tld" # Still email + assert "token" not in mock_legacy_config_entry.data # Still legacy data + + +@pytest.mark.usefixtures("client_credentials") +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.usefixtures("mock_setup_entry") +async def test_legacy_migration_no_email_in_jwt( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_legacy_config_entry: MockConfigEntry, + jwt: str, # JWT with empty email array +) -> None: + """Test migration from legacy config succeeds when JWT has no email (can't validate).""" + + mock_legacy_config_entry.add_to_hass(hass) + + # Start reauth flow + mock_legacy_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "auth" + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": jwt, # JWT with email: [] + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "user_id": USER_ID, + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + # Verify the entry was updated (allowed because no email to validate) + assert mock_legacy_config_entry.unique_id == USER_ID # Updated from email to userId + assert "token" in mock_legacy_config_entry.data + assert mock_legacy_config_entry.data["auth_implementation"] == "august" + assert mock_legacy_config_entry.data["token"]["access_token"] == jwt diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py index cdc538ca6bd..b45cf8d22f7 100644 --- a/tests/components/august/test_diagnostics.py +++ b/tests/components/august/test_diagnostics.py @@ -25,7 +25,7 @@ async def test_diagnostics( ) doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") - entry, _ = await _create_august_api_with_devices(hass, [lock_one, doorbell_one]) + entry, _, _ = await _create_august_api_with_devices(hass, [lock_one, doorbell_one]) diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert diag == snapshot diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py deleted file mode 100644 index 1603aeb3ecb..00000000000 --- a/tests/components/august/test_gateway.py +++ /dev/null @@ -1,54 +0,0 @@ -"""The gateway tests for the august platform.""" - -from pathlib import Path -from unittest.mock import MagicMock, patch - -from yalexs.authenticator_common import AuthenticationState - -from homeassistant.components.august.const import DOMAIN -from homeassistant.components.august.gateway import AugustGateway -from homeassistant.core import HomeAssistant - -from .mocks import _mock_august_authentication, _mock_get_config - - -async def test_refresh_access_token(hass: HomeAssistant) -> None: - """Test token refreshes.""" - await _patched_refresh_access_token(hass, "new_token", 5678) - - -@patch("yalexs.manager.gateway.ApiAsync.async_get_operable_locks") -@patch("yalexs.manager.gateway.AuthenticatorAsync.async_authenticate") -@patch("yalexs.manager.gateway.AuthenticatorAsync.should_refresh") -@patch("yalexs.manager.gateway.AuthenticatorAsync.async_refresh_access_token") -async def _patched_refresh_access_token( - hass: HomeAssistant, - new_token: str, - new_token_expire_time: int, - refresh_access_token_mock, - should_refresh_mock, - authenticate_mock, - async_get_operable_locks_mock, -) -> None: - authenticate_mock.side_effect = MagicMock( - return_value=_mock_august_authentication( - "original_token", 1234, AuthenticationState.AUTHENTICATED - ) - ) - august_gateway = AugustGateway(Path(hass.config.config_dir), MagicMock()) - mocked_config = _mock_get_config() - await august_gateway.async_setup(mocked_config[DOMAIN]) - await august_gateway.async_authenticate() - - should_refresh_mock.return_value = False - await august_gateway.async_refresh_access_token_if_needed() - refresh_access_token_mock.assert_not_called() - - should_refresh_mock.return_value = True - refresh_access_token_mock.return_value = _mock_august_authentication( - new_token, new_token_expire_time, AuthenticationState.AUTHENTICATED - ) - await august_gateway.async_refresh_access_token_if_needed() - refresh_access_token_mock.assert_called() - assert await august_gateway.async_get_access_token() == new_token - assert august_gateway.authentication.access_token_expires == new_token_expire_time diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 3343e85d60a..33517e9e130 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,12 +1,11 @@ """The tests for the august platform.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock from aiohttp import ClientResponseError import pytest -from yalexs.authenticator_common import AuthenticationState from yalexs.const import Brand -from yalexs.exceptions import AugustApiAIOHTTPError +from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth from homeassistant.components.august.const import DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState @@ -29,10 +28,8 @@ from homeassistant.setup import async_setup_component from .mocks import ( _create_august_with_devices, - _mock_august_authentication, _mock_doorsense_enabled_august_lock_detail, _mock_doorsense_missing_august_lock_detail, - _mock_get_config, _mock_inoperative_august_lock_detail, _mock_lock_with_offline_key, _mock_operative_august_lock_detail, @@ -44,68 +41,36 @@ from tests.typing import WebSocketGenerator async def test_august_api_is_failing(hass: HomeAssistant) -> None: """Config entry state is SETUP_RETRY when august api is failing.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", + config_entry, _ = await _create_august_with_devices( + hass, + authenticate_side_effect=AugustApiAIOHTTPError( + "offline", ClientResponseError(None, None, status=500) + ), ) - config_entry.add_to_hass(hass) - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - side_effect=ClientResponseError(None, None, status=500), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_august_is_offline(hass: HomeAssistant) -> None: """Config entry state is SETUP_RETRY when august is offline.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", + config_entry, _ = await _create_august_with_devices( + hass, authenticate_side_effect=TimeoutError ) - config_entry.add_to_hass(hass) - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - side_effect=TimeoutError, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_august_late_auth_failure(hass: HomeAssistant) -> None: """Test we can detect a late auth failure.""" - aiohttp_client_response_exception = ClientResponseError(None, None, status=401) - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", - ) - config_entry.add_to_hass(hass) - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - side_effect=AugustApiAIOHTTPError( - "This should bubble up as its user consumable", - aiohttp_client_response_exception, + config_entry, _ = await _create_august_with_devices( + hass, + authenticate_side_effect=InvalidAuth( + "authfailed", ClientResponseError(None, None, status=401) ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + ) assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() - assert flows[0]["step_id"] == "reauth_validate" + assert flows[0]["step_id"] == "pick_implementation" async def test_unlock_throws_august_api_http_error(hass: HomeAssistant) -> None: @@ -210,163 +175,12 @@ async def test_lock_has_doorsense(hass: HomeAssistant) -> None: assert binary_sensor_missing_doorsense_id_name_open is None -async def test_auth_fails(hass: HomeAssistant) -> None: - """Config entry state is SETUP_ERROR when auth fails.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", - ) - config_entry.add_to_hass(hass) - assert hass.config_entries.flow.async_progress() == [] - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - side_effect=ClientResponseError(None, None, status=401), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.SETUP_ERROR - - flows = hass.config_entries.flow.async_progress() - - assert flows[0]["step_id"] == "reauth_validate" - - -async def test_bad_password(hass: HomeAssistant) -> None: - """Config entry state is SETUP_ERROR when the password has been changed.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", - ) - config_entry.add_to_hass(hass) - assert hass.config_entries.flow.async_progress() == [] - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - return_value=_mock_august_authentication( - "original_token", 1234, AuthenticationState.BAD_PASSWORD - ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.SETUP_ERROR - - flows = hass.config_entries.flow.async_progress() - - assert flows[0]["step_id"] == "reauth_validate" - - -async def test_http_failure(hass: HomeAssistant) -> None: - """Config entry state is SETUP_RETRY when august is offline.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", - ) - config_entry.add_to_hass(hass) - assert hass.config_entries.flow.async_progress() == [] - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - side_effect=ClientResponseError(None, None, status=500), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.SETUP_RETRY - - assert hass.config_entries.flow.async_progress() == [] - - -async def test_unknown_auth_state(hass: HomeAssistant) -> None: - """Config entry state is SETUP_ERROR when august is in an unknown auth state.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", - ) - config_entry.add_to_hass(hass) - assert hass.config_entries.flow.async_progress() == [] - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - return_value=_mock_august_authentication("original_token", 1234, None), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.SETUP_ERROR - - flows = hass.config_entries.flow.async_progress() - - assert flows[0]["step_id"] == "reauth_validate" - - -async def test_requires_validation_state(hass: HomeAssistant) -> None: - """Config entry state is SETUP_ERROR when august requires validation.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", - ) - config_entry.add_to_hass(hass) - assert hass.config_entries.flow.async_progress() == [] - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - return_value=_mock_august_authentication( - "original_token", 1234, AuthenticationState.REQUIRES_VALIDATION - ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.SETUP_ERROR - - assert len(hass.config_entries.flow.async_progress()) == 1 - assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth" - - -async def test_unknown_auth_http_401(hass: HomeAssistant) -> None: - """Config entry state is SETUP_ERROR when august gets an http.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=_mock_get_config()[DOMAIN], - title="August august", - ) - config_entry.add_to_hass(hass) - assert hass.config_entries.flow.async_progress() == [] - - with patch( - "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", - return_value=_mock_august_authentication("original_token", 1234, None), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.SETUP_ERROR - - flows = hass.config_entries.flow.async_progress() - - assert flows[0]["step_id"] == "reauth_validate" - - async def test_load_unload(hass: HomeAssistant) -> None: """Config entry can be unloaded.""" august_operative_lock = await _mock_operative_august_lock_detail(hass) august_inoperative_lock = await _mock_inoperative_august_lock_detail(hass) - config_entry = await _create_august_with_devices( + config_entry, _ = await _create_august_with_devices( hass, [august_operative_lock, august_inoperative_lock] ) @@ -385,7 +199,7 @@ async def test_load_triggers_ble_discovery( august_lock_with_key = await _mock_lock_with_offline_key(hass) august_lock_without_key = await _mock_operative_august_lock_detail(hass) - config_entry = await _create_august_with_devices( + config_entry, _ = await _create_august_with_devices( hass, [august_lock_with_key, august_lock_without_key] ) await hass.async_block_till_done() @@ -410,7 +224,7 @@ async def test_device_remove_devices( """Test we can only remove a device that no longer exists.""" assert await async_setup_component(hass, "config", {}) august_operative_lock = await _mock_operative_august_lock_detail(hass) - config_entry = await _create_august_with_devices(hass, [august_operative_lock]) + config_entry, _ = await _create_august_with_devices(hass, [august_operative_lock]) entity = entity_registry.entities["lock.a6697750d607098bae8d6baa11ef8063_name"] device_entry = device_registry.async_get(entity.device_id) @@ -427,21 +241,46 @@ async def test_device_remove_devices( async def test_brand_migration_issue(hass: HomeAssistant) -> None: - """Test creating and removing the brand migration issue.""" + """Test removing the brand migration issue.""" august_operative_lock = await _mock_operative_august_lock_detail(hass) - config_entry = await _create_august_with_devices( + config_entry, _ = await _create_august_with_devices( hass, [august_operative_lock], brand=Brand.YALE_HOME ) assert config_entry.state is ConfigEntryState.LOADED issue_reg = ir.async_get(hass) - issue_entry = issue_reg.async_get_issue(DOMAIN, "yale_brand_migration") - assert issue_entry - assert issue_entry.severity == ir.IssueSeverity.CRITICAL - assert issue_entry.translation_placeholders == { - "migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale" - } await hass.config_entries.async_remove(config_entry.entry_id) assert not issue_reg.async_get_issue(DOMAIN, "yale_brand_migration") + + +async def test_oauth_migration_on_legacy_entry(hass: HomeAssistant) -> None: + """Test that legacy config entry triggers OAuth migration.""" + # Create a legacy config entry without auth_implementation + legacy_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "login_method": "email", + "username": "test@example.com", + "password": "test-password", + "install_id": None, + "timeout": 10, + "access_token_cache_file": ".test@example.com.august.conf", + }, + unique_id="test@example.com", + ) + legacy_entry.add_to_hass(hass) + + # Try to setup the entry - should fail with auth error and trigger reauth + await hass.config_entries.async_setup(legacy_entry.entry_id) + await hass.async_block_till_done() + + # Entry should be in setup_error state + assert legacy_entry.state is ConfigEntryState.SETUP_ERROR + + # A reauth flow should be started + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "pick_implementation" + assert flows[0]["context"]["source"] == "reauth" diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index a1ba83ecb01..cd4761c5574 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -374,7 +374,7 @@ async def test_lock_update_via_pubnub(hass: HomeAssistant) -> None: pubnub = AugustPubNub() activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json") - config_entry = await _create_august_with_devices( + config_entry, _ = await _create_august_with_devices( hass, [lock_one], activities=activities, pubnub=pubnub ) pubnub.connected = True diff --git a/tests/components/backup/snapshots/test_diagnostics.ambr b/tests/components/backup/snapshots/test_diagnostics.ambr index cf412970204..a1ee55f07f1 100644 --- a/tests/components/backup/snapshots/test_diagnostics.ambr +++ b/tests/components/backup/snapshots/test_diagnostics.ambr @@ -31,7 +31,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index aa9ccde4b8a..b82bb7c650f 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -88,7 +88,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -187,7 +186,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -306,7 +304,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -413,7 +410,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -520,7 +516,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -633,7 +628,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -758,7 +752,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 31e7fa0ee5b..2bac144a258 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -1306,7 +1306,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -1423,7 +1422,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': None, }), }), @@ -1540,7 +1538,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': None, }), }), @@ -1671,7 +1668,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -1949,7 +1945,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -2064,7 +2059,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -2179,7 +2173,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -2296,7 +2289,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': '06:00:00', }), }), @@ -2415,7 +2407,6 @@ 'mon', ]), 'recurrence': 'custom_days', - 'state': 'never', 'time': None, }), }), @@ -2532,7 +2523,6 @@ 'days': list([ ]), 'recurrence': 'never', - 'state': 'never', 'time': None, }), }), @@ -2653,7 +2643,6 @@ 'sun', ]), 'recurrence': 'custom_days', - 'state': 'never', 'time': None, }), }), @@ -2778,7 +2767,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': None, }), }), @@ -2895,7 +2883,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': None, }), }), @@ -3012,7 +2999,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': None, }), }), @@ -3129,7 +3115,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': None, }), }), @@ -3246,7 +3231,6 @@ 'days': list([ ]), 'recurrence': 'daily', - 'state': 'never', 'time': None, }), }), diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 02e40cabb33..ba19abdbb34 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -76,7 +76,7 @@ DEFAULT_STORAGE_DATA: dict[str, Any] = { "copies": None, "days": None, }, - "schedule": {"days": [], "recurrence": "never", "state": "never", "time": None}, + "schedule": {"days": [], "recurrence": "never", "time": None}, }, } DAILY = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] @@ -1009,7 +1009,6 @@ async def test_agents_info( "schedule": { "days": DAILY, "recurrence": "custom_days", - "state": "never", "time": None, }, }, @@ -1041,7 +1040,6 @@ async def test_agents_info( "schedule": { "days": [], "recurrence": "never", - "state": "never", "time": None, }, }, @@ -1073,7 +1071,6 @@ async def test_agents_info( "schedule": { "days": [], "recurrence": "never", - "state": "never", "time": None, }, }, @@ -1105,7 +1102,6 @@ async def test_agents_info( "schedule": { "days": ["mon"], "recurrence": "custom_days", - "state": "never", "time": None, }, }, @@ -1137,7 +1133,6 @@ async def test_agents_info( "schedule": { "days": [], "recurrence": "never", - "state": "never", "time": None, }, }, @@ -1169,7 +1164,6 @@ async def test_agents_info( "schedule": { "days": ["mon", "sun"], "recurrence": "custom_days", - "state": "never", "time": None, }, }, @@ -1204,7 +1198,6 @@ async def test_agents_info( "schedule": { "days": ["mon", "sun"], "recurrence": "custom_days", - "state": "never", "time": None, }, }, @@ -1236,7 +1229,6 @@ async def test_agents_info( "schedule": { "days": [], "recurrence": "never", - "state": "never", "time": None, }, }, @@ -1268,7 +1260,6 @@ async def test_agents_info( "schedule": { "days": [], "recurrence": "never", - "state": "never", "time": None, }, }, @@ -1309,7 +1300,6 @@ async def test_agents_info( "schedule": { "days": ["mon", "sun"], "recurrence": "custom_days", - "state": "never", "time": None, }, }, @@ -1960,7 +1950,6 @@ async def test_config_schedule_logic( "schedule": { "days": [], "recurrence": "daily", - "state": "never", "time": None, }, }, @@ -2870,7 +2859,6 @@ async def test_config_retention_copies_logic( "schedule": { "days": [], "recurrence": "daily", - "state": "never", "time": None, }, }, @@ -3149,7 +3137,6 @@ async def test_config_retention_copies_logic_manual_backup( "schedule": { "days": [], "recurrence": "daily", - "state": "never", "time": None, }, }, @@ -3814,7 +3801,6 @@ async def test_config_retention_days_logic( "schedule": { "days": [], "recurrence": "never", - "state": "never", "time": None, }, }, @@ -3886,7 +3872,6 @@ async def test_configured_agents_unavailable_repair( "schedule": { "days": ["mon"], "recurrence": "custom_days", - "state": "never", "time": None, }, }, diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 59fbdf9a253..254c5428806 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -182,7 +182,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -212,7 +217,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index dd71c1e5d06..e9ad5d0a1e1 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -185,7 +185,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -215,7 +220,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index d439f46bb71..6951a2ce4cc 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -145,6 +145,7 @@ def inject_advertisement_with_time_and_source_connectable( time: float, source: str, connectable: bool, + raw: bytes | None = None, ) -> None: """Inject an advertisement into the manager from a specific source at a time and connectable status.""" async_get_advertisement_callback(hass)( @@ -161,6 +162,7 @@ def inject_advertisement_with_time_and_source_connectable( connectable=connectable, time=time, tx_power=adv.tx_power, + raw=raw, ) ) diff --git a/tests/components/bluetooth/test_websocket_api.py b/tests/components/bluetooth/test_websocket_api.py index 2e613932f3c..f12d77913a9 100644 --- a/tests/components/bluetooth/test_websocket_api.py +++ b/tests/components/bluetooth/test_websocket_api.py @@ -22,6 +22,7 @@ from . import ( generate_advertisement_data, generate_ble_device, inject_advertisement_with_source, + inject_advertisement_with_time_and_source_connectable, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -72,6 +73,7 @@ async def test_subscribe_advertisements( "source": HCI0_SOURCE_ADDRESS, "time": ANY, "tx_power": -127, + "raw": None, } ] } @@ -83,8 +85,15 @@ async def test_subscribe_advertisements( service_uuids=[], rssi=-80, ) - inject_advertisement_with_source( - hass, switchbot_device_signal_100, switchbot_adv_signal_100, HCI1_SOURCE_ADDRESS + # Inject with raw bytes data + inject_advertisement_with_time_and_source_connectable( + hass, + switchbot_device_signal_100, + switchbot_adv_signal_100, + time.monotonic(), + HCI1_SOURCE_ADDRESS, + True, + raw=b"\x02\x01\x06\x03\x03\x0f\x18", ) async with asyncio.timeout(1): response = await client.receive_json() @@ -101,6 +110,7 @@ async def test_subscribe_advertisements( "source": HCI1_SOURCE_ADDRESS, "time": ANY, "tx_power": -127, + "raw": "02010603030f18", } ] } diff --git a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr index e3444777ff0..7e1604127e2 100644 --- a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr @@ -168,7 +168,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'AC Failure', + 'original_name': 'AC failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'suggested_object_id': None, @@ -182,7 +182,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch AMAX 3000 AC Failure', + 'friendly_name': 'Bosch AMAX 3000 AC failure', }), 'context': , 'entity_id': 'binary_sensor.bosch_amax_3000_ac_failure', @@ -1187,7 +1187,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'AC Failure', + 'original_name': 'AC failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'suggested_object_id': None, @@ -1201,7 +1201,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) AC Failure', + 'friendly_name': 'Bosch B5512 (US1B) AC failure', }), 'context': , 'entity_id': 'binary_sensor.bosch_b5512_us1b_ac_failure', @@ -2206,7 +2206,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'AC Failure', + 'original_name': 'AC failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'suggested_object_id': None, @@ -2220,7 +2220,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 AC Failure', + 'friendly_name': 'Bosch Solution 3000 AC failure', }), 'context': , 'entity_id': 'binary_sensor.bosch_solution_3000_ac_failure', diff --git a/tests/components/braviatv/snapshots/test_diagnostics.ambr b/tests/components/braviatv/snapshots/test_diagnostics.ambr index de76c00cd23..e6bc20a2216 100644 --- a/tests/components/braviatv/snapshots/test_diagnostics.ambr +++ b/tests/components/braviatv/snapshots/test_diagnostics.ambr @@ -21,7 +21,7 @@ 'source': 'user', 'subentries': list([ ]), - 'title': 'Mock Title', + 'title': 'BRAVIA TV-Model', 'unique_id': 'very_unique_string', 'version': 1, }), diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 497e88053f5..e59d0b6805b 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -143,7 +143,7 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" - assert result["title"] == "TV-Model" + assert result["title"] == "BRAVIA TV-Model" assert result["data"] == { CONF_HOST: "bravia-host", CONF_PIN: "1234", @@ -340,7 +340,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" - assert result["title"] == "TV-Model" + assert result["title"] == "BRAVIA TV-Model" assert result["data"] == { CONF_HOST: "bravia-host", CONF_PIN: "1234", @@ -381,7 +381,7 @@ async def test_create_entry_psk(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" - assert result["title"] == "TV-Model" + assert result["title"] == "BRAVIA TV-Model" assert result["data"] == { CONF_HOST: "bravia-host", CONF_PIN: "mypsk", diff --git a/tests/components/braviatv/test_diagnostics.py b/tests/components/braviatv/test_diagnostics.py index 2f6df722909..ecaa82678e6 100644 --- a/tests/components/braviatv/test_diagnostics.py +++ b/tests/components/braviatv/test_diagnostics.py @@ -46,6 +46,7 @@ async def test_entry_diagnostics( config_entry = MockConfigEntry( domain=DOMAIN, + title="BRAVIA TV-Model", data={ CONF_HOST: "localhost", CONF_MAC: "AA:BB:CC:DD:EE:FF", diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index cc52799d28b..10945a24878 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -2,8 +2,9 @@ from unittest.mock import MagicMock -from bsblan import BSBLANAuthError, BSBLANConnectionError +from bsblan import BSBLANAuthError, BSBLANConnectionError, BSBLANError from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.bsblan.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -75,3 +76,38 @@ async def test_config_entry_auth_failed_triggers_reauth( assert len(flows) == 1 assert flows[0]["context"]["source"] == "reauth" assert flows[0]["context"]["entry_id"] == mock_config_entry.entry_id + + +@pytest.mark.parametrize( + ("method", "exception", "expected_state"), + [ + ( + "device", + BSBLANConnectionError("Connection failed"), + ConfigEntryState.SETUP_RETRY, + ), + ( + "info", + BSBLANAuthError("Authentication failed"), + ConfigEntryState.SETUP_ERROR, + ), + ("static_values", BSBLANError("General error"), ConfigEntryState.SETUP_ERROR), + ], +) +async def test_config_entry_static_data_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_bsblan: MagicMock, + method: str, + exception: Exception, + expected_state: ConfigEntryState, +) -> None: + """Test various errors during static data fetching trigger appropriate config entry states.""" + # Mock the specified method to raise the exception + getattr(mock_bsblan, method).side_effect = exception + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is expected_state diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index f1cffa8583f..63fdece9c98 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -253,14 +253,14 @@ _LOGGER = logging.getLogger(__name__) { "sensor_entity": "sensor.test_device_18b2_pm10", "friendly_name": "Test Device 18B2 Pm10", - "unit_of_measurement": "µg/m³", + "unit_of_measurement": "μg/m³", "state_class": "measurement", "expected_state": "7170", }, { "sensor_entity": "sensor.test_device_18b2_pm25", "friendly_name": "Test Device 18B2 Pm25", - "unit_of_measurement": "µg/m³", + "unit_of_measurement": "μg/m³", "state_class": "measurement", "expected_state": "3090", }, @@ -296,7 +296,7 @@ _LOGGER = logging.getLogger(__name__) "sensor.test_device_18b2_volatile_organic_compounds" ), "friendly_name": "Test Device 18B2 Volatile Organic Compounds", - "unit_of_measurement": "µg/m³", + "unit_of_measurement": "μg/m³", "state_class": "measurement", "expected_state": "307", }, @@ -607,14 +607,14 @@ async def test_v1_sensors( { "sensor_entity": "sensor.test_device_18b2_pm10", "friendly_name": "Test Device 18B2 Pm10", - "unit_of_measurement": "µg/m³", + "unit_of_measurement": "μg/m³", "state_class": "measurement", "expected_state": "7170", }, { "sensor_entity": "sensor.test_device_18b2_pm25", "friendly_name": "Test Device 18B2 Pm25", - "unit_of_measurement": "µg/m³", + "unit_of_measurement": "μg/m³", "state_class": "measurement", "expected_state": "3090", }, @@ -650,7 +650,7 @@ async def test_v1_sensors( "sensor.test_device_18b2_volatile_organic_compounds" ), "friendly_name": "Test Device 18B2 Volatile Organic Compounds", - "unit_of_measurement": "µg/m³", + "unit_of_measurement": "μg/m³", "state_class": "measurement", "expected_state": "307", }, diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 4b5a578ecc4..06072b88afe 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -354,7 +354,12 @@ async def test_get_trigger_capabilities_hvac_mode(hass: HomeAssistant) -> None: "required": True, "type": "select", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] @@ -389,13 +394,20 @@ async def test_get_trigger_capabilities_temp_humid( "description": {"suffix": suffix}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": suffix}, "name": "below", "optional": True, + "required": False, "type": "float", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index a4625fcce92..10d38c227f1 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -55,7 +55,10 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock]: # Attributes set in the constructor without parameters. # We spec the mocks with the real classes # and set constructor attributes or mock properties as needed. - mock_cloud.google_report_state = MagicMock(spec=GoogleReportState) + mock_cloud.google_report_state = MagicMock( + spec=GoogleReportState, + request_sync=AsyncMock(), + ) mock_cloud.cloudhooks = MagicMock(spec=Cloudhooks) mock_cloud.remote = MagicMock( spec=RemoteUI, diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index ef7a99453f0..7fc8c73785b 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -3,6 +3,11 @@ import contextlib from unittest.mock import AsyncMock, Mock, patch +from hass_nabucasa.alexa_api import ( + AlexaApiError, + AlexaApiNeedsRelinkError, + AlexaApiNoTokenError, +) import pytest from homeassistant.components.alexa import errors @@ -195,30 +200,40 @@ async def test_alexa_config_invalidate_token( servicehandlers_server="example", auth=Mock(async_check_token=AsyncMock()), websession=async_get_clientsession(hass), + alexa_api=Mock( + access_token=AsyncMock( + return_value={ + "access_token": "mock-token", + "event_endpoint": "http://example.com/alexa_endpoint", + "expires_in": 30, + } + ) + ), ), ) token = await conf.async_get_access_token() assert token == "mock-token" - assert len(aioclient_mock.mock_calls) == 1 + assert len(conf._cloud.alexa_api.access_token.mock_calls) == 1 token = await conf.async_get_access_token() assert token == "mock-token" - assert len(aioclient_mock.mock_calls) == 1 + assert len(conf._cloud.alexa_api.access_token.mock_calls) == 1 assert conf._token_valid is not None conf.async_invalidate_access_token() assert conf._token_valid is None token = await conf.async_get_access_token() assert token == "mock-token" - assert len(aioclient_mock.mock_calls) == 2 + assert len(conf._cloud.alexa_api.access_token.mock_calls) == 2 @pytest.mark.parametrize( - ("reject_reason", "expected_exception"), + ("lib_exception", "expected_exception"), [ - ("RefreshTokenNotFound", errors.RequireRelink), - ("UnknownRegion", errors.RequireRelink), - ("OtherReason", errors.NoTokenAvailable), + (AlexaApiNeedsRelinkError("Needs relink"), errors.RequireRelink), + (AlexaApiNeedsRelinkError("UnknownRegion"), errors.RequireRelink), + (AlexaApiNoTokenError("OtherReason"), errors.NoTokenAvailable), + (AlexaApiError("OtherReason"), errors.NoTokenAvailable), ], ) async def test_alexa_config_fail_refresh_token( @@ -226,7 +241,7 @@ async def test_alexa_config_fail_refresh_token( cloud_prefs: CloudPreferences, aioclient_mock: AiohttpClientMocker, entity_registry: er.EntityRegistry, - reject_reason: str, + lib_exception: Exception, expected_exception: type[Exception], ) -> None: """Test Alexa config failing to refresh token.""" @@ -259,6 +274,15 @@ async def test_alexa_config_fail_refresh_token( servicehandlers_server="example", auth=Mock(async_check_token=AsyncMock()), websession=async_get_clientsession(hass), + alexa_api=Mock( + access_token=AsyncMock( + return_value={ + "access_token": "mock-token", + "event_endpoint": "http://example.com/alexa_endpoint", + "expires_in": 30, + } + ) + ), ), ) await conf.async_initialize() @@ -284,12 +308,7 @@ async def test_alexa_config_fail_refresh_token( # Invalidate the token and try to fetch another conf.async_invalidate_access_token() - aioclient_mock.clear_requests() - aioclient_mock.post( - "https://example/alexa/access_token", - json={"reason": reject_reason}, - status=400, - ) + conf._cloud.alexa_api.access_token.side_effect = lib_exception # Change states to trigger event listener hass.states.async_set(entity_entry.entity_id, "off") @@ -310,15 +329,8 @@ async def test_alexa_config_fail_refresh_token( # Simulate we're again authorized and token update succeeds # State reporting should now be re-enabled for Alexa - aioclient_mock.clear_requests() - aioclient_mock.post( - "https://example/alexa/access_token", - json={ - "access_token": "mock-token", - "event_endpoint": "http://example.com/alexa_endpoint", - "expires_in": 30, - }, - ) + conf._cloud.alexa_api.access_token.side_effect = None + await conf.set_authorized(True) assert cloud_prefs.alexa_report_state is True assert conf.should_report_state is True diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index cb456be5036..e6fb289d09e 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,7 +1,7 @@ """Test the Cloud Google Config.""" from http import HTTPStatus -from unittest.mock import Mock, PropertyMock, patch +from unittest.mock import AsyncMock, Mock, PropertyMock, patch from freezegun import freeze_time import pytest @@ -119,15 +119,13 @@ async def test_sync_entities( assert len(mock_conf.async_get_agent_users()) == 1 - with patch( - "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=Mock(status=HTTPStatus.NOT_FOUND), - ) as mock_request_sync: - assert ( - await mock_conf.async_sync_entities("mock-user-id") == HTTPStatus.NOT_FOUND - ) - assert len(mock_conf.async_get_agent_users()) == 0 - assert len(mock_request_sync.mock_calls) == 1 + mock_conf._cloud.google_report_state.request_sync = AsyncMock( + return_value=Mock(status=HTTPStatus.NOT_FOUND) + ) + + assert await mock_conf.async_sync_entities("mock-user-id") == HTTPStatus.NOT_FOUND + assert len(mock_conf.async_get_agent_users()) == 0 + assert len(mock_conf._cloud.google_report_state.request_sync.mock_calls) == 1 async def test_google_update_expose_trigger_sync( diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index f125a5cbdae..96927477b0a 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -139,31 +139,34 @@ async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None: async def test_google_actions_sync( setup_cloud: None, hass_client: ClientSessionGenerator, + cloud: MagicMock, ) -> None: """Test syncing Google Actions.""" cloud_client = await hass_client() - with patch( - "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=Mock(status=200), - ) as mock_request_sync: - req = await cloud_client.post("/api/cloud/google_actions/sync") - assert req.status == HTTPStatus.OK - assert mock_request_sync.call_count == 1 + + cloud.google_report_state.request_sync = AsyncMock( + return_value=Mock(status=HTTPStatus.OK) + ) + + req = await cloud_client.post("/api/cloud/google_actions/sync") + assert req.status == HTTPStatus.OK + assert len(cloud.google_report_state.request_sync.mock_calls) == 1 async def test_google_actions_sync_fails( setup_cloud: None, hass_client: ClientSessionGenerator, + cloud: MagicMock, ) -> None: """Test syncing Google Actions gone bad.""" cloud_client = await hass_client() - with patch( - "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=Mock(status=HTTPStatus.INTERNAL_SERVER_ERROR), - ) as mock_request_sync: - req = await cloud_client.post("/api/cloud/google_actions/sync") - assert req.status == HTTPStatus.INTERNAL_SERVER_ERROR - assert mock_request_sync.call_count == 1 + cloud.google_report_state.request_sync = AsyncMock( + return_value=Mock(status=HTTPStatus.INTERNAL_SERVER_ERROR) + ) + + req = await cloud_client.post("/api/cloud/google_actions/sync") + assert req.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert len(cloud.google_report_state.request_sync.mock_calls) == 1 @pytest.mark.parametrize( diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 0a2475ac218..be538c7a42d 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -5,7 +5,7 @@ from homeassistant.components.coinbase.const import ( CONF_EXCHANGE_RATES, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant from .const import ( @@ -65,7 +65,7 @@ class MockGetAccountsV3: start = ids.index(cursor) if cursor else 0 has_next = (target_end := start + 2) < len(MOCK_ACCOUNTS_RESPONSE_V3) - end = target_end if has_next else -1 + end = target_end if has_next else len(MOCK_ACCOUNTS_RESPONSE_V3) next_cursor = ids[end] if has_next else ids[-1] self.accounts = { "accounts": MOCK_ACCOUNTS_RESPONSE_V3[start:end], @@ -120,31 +120,6 @@ async def init_mock_coinbase( hass: HomeAssistant, currencies: list[str] | None = None, rates: list[str] | None = None, -) -> MockConfigEntry: - """Init Coinbase integration for testing.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - entry_id="080272b77a4f80c41b94d7cdc86fd826", - unique_id=None, - title="Test User", - data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, - options={ - CONF_CURRENCIES: currencies or [], - CONF_EXCHANGE_RATES: rates or [], - }, - ) - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -async def init_mock_coinbase_v3( - hass: HomeAssistant, - currencies: list[str] | None = None, - rates: list[str] | None = None, ) -> MockConfigEntry: """Init Coinbase integration for testing.""" config_entry = MockConfigEntry( @@ -155,7 +130,6 @@ async def init_mock_coinbase_v3( data={ CONF_API_KEY: "organizations/123456", CONF_API_TOKEN: "AbCDeF", - CONF_API_VERSION: "v3", }, options={ CONF_CURRENCIES: currencies or [], diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index aa2c6208e0f..3858df83269 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -3,9 +3,8 @@ import logging from unittest.mock import patch -from coinbase.wallet.error import AuthenticationError +from coinbase.rest.rest_base import HTTPError import pytest -from requests.models import Response from homeassistant import config_entries from homeassistant.components.coinbase.const import ( @@ -14,17 +13,14 @@ from homeassistant.components.coinbase.const import ( CONF_EXCHANGE_RATES, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from .common import ( init_mock_coinbase, - init_mock_coinbase_v3, - mock_get_current_user, mock_get_exchange_rates, mock_get_portfolios, - mocked_get_accounts, mocked_get_accounts_v3, ) from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHANGE_RATE @@ -41,13 +37,13 @@ async def test_form(hass: HomeAssistant) -> None: with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), patch( "homeassistant.components.coinbase.async_setup_entry", @@ -61,11 +57,10 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Test User" + assert result2["title"] == "Default" assert result2["data"] == { CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF", - CONF_API_VERSION: "v2", } assert len(mock_setup_entry.mock_calls) == 1 @@ -80,16 +75,9 @@ async def test_form_invalid_auth( caplog.set_level(logging.DEBUG) - response = Response() - response.status_code = 401 - api_auth_error_unknown = AuthenticationError( - response, - "authentication_error", - "unknown error", - [{"id": "authentication_error", "message": "unknown error"}], - ) + api_auth_error_unknown = HTTPError("unknown error") with patch( - "coinbase.wallet.client.Client.get_current_user", + "coinbase.rest.RESTClient.get_portfolios", side_effect=api_auth_error_unknown, ): result2 = await hass.config_entries.flow.async_configure( @@ -104,14 +92,9 @@ async def test_form_invalid_auth( assert result2["errors"] == {"base": "invalid_auth"} assert "Coinbase rejected API credentials due to an unknown error" in caplog.text - api_auth_error_key = AuthenticationError( - response, - "authentication_error", - "invalid api key", - [{"id": "authentication_error", "message": "invalid api key"}], - ) + api_auth_error_key = HTTPError("invalid api key") with patch( - "coinbase.wallet.client.Client.get_current_user", + "coinbase.rest.RESTClient.get_portfolios", side_effect=api_auth_error_key, ): result2 = await hass.config_entries.flow.async_configure( @@ -126,14 +109,9 @@ async def test_form_invalid_auth( assert result2["errors"] == {"base": "invalid_auth_key"} assert "Coinbase rejected API credentials due to an invalid API key" in caplog.text - api_auth_error_secret = AuthenticationError( - response, - "authentication_error", - "invalid signature", - [{"id": "authentication_error", "message": "invalid signature"}], - ) + api_auth_error_secret = HTTPError("invalid signature") with patch( - "coinbase.wallet.client.Client.get_current_user", + "coinbase.rest.RESTClient.get_portfolios", side_effect=api_auth_error_secret, ): result2 = await hass.config_entries.flow.async_configure( @@ -158,7 +136,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) with patch( - "coinbase.wallet.client.Client.get_current_user", + "coinbase.rest.RESTClient.get_portfolios", side_effect=ConnectionError, ): result2 = await hass.config_entries.flow.async_configure( @@ -180,7 +158,7 @@ async def test_form_catch_all_exception(hass: HomeAssistant) -> None: ) with patch( - "coinbase.wallet.client.Client.get_current_user", + "coinbase.rest.RESTClient.get_portfolios", side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( @@ -200,17 +178,14 @@ async def test_option_form(hass: HomeAssistant) -> None: with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), - patch( - "homeassistant.components.coinbase.update_listener" - ) as mock_update_listener, ): config_entry = await init_mock_coinbase(hass) await hass.async_block_till_done() @@ -226,20 +201,19 @@ async def test_option_form(hass: HomeAssistant) -> None: ) assert result2["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() - assert len(mock_update_listener.mock_calls) == 1 async def test_form_bad_account_currency(hass: HomeAssistant) -> None: """Test we handle a bad currency option.""" with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), ): config_entry = await init_mock_coinbase(hass) @@ -262,13 +236,13 @@ async def test_form_bad_exchange_rate(hass: HomeAssistant) -> None: """Test we handle a bad exchange rate.""" with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), ): config_entry = await init_mock_coinbase(hass) @@ -290,13 +264,13 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None: """Test we handle an unknown exception in the option flow.""" with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), ): config_entry = await init_mock_coinbase(hass) @@ -304,7 +278,7 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None: await hass.async_block_till_done() with patch( - "coinbase.wallet.client.Client.get_accounts", + "coinbase.rest.RESTClient.get_accounts", side_effect=Exception, ): result2 = await hass.config_entries.options.async_configure( @@ -320,75 +294,99 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} -async def test_form_v3(hass: HomeAssistant) -> None: - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test reauth flow.""" with ( - patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( "coinbase.rest.RESTClient.get_portfolios", return_value=mock_get_portfolios(), ), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.rest.RESTBase.get", + "coinbase.rest.RESTClient.get", return_value={"data": mock_get_exchange_rates()}, ), + ): + config_entry = await init_mock_coinbase(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # Test successful reauth + with ( patch( - "homeassistant.components.coinbase.async_setup_entry", - return_value=True, - ) as mock_setup_entry, + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), + ), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), + patch( + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_API_KEY: "organizations/123456", CONF_API_TOKEN: "AbCDeF"}, + { + CONF_API_KEY: "new_key", + CONF_API_TOKEN: "new_secret", + }, ) - await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Default" - assert result2["data"] == { - CONF_API_KEY: "organizations/123456", - CONF_API_TOKEN: "AbCDeF", - CONF_API_VERSION: "v3", - } - assert len(mock_setup_entry.mock_calls) == 1 + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert config_entry.data[CONF_API_KEY] == "new_key" + assert config_entry.data[CONF_API_TOKEN] == "new_secret" -async def test_option_form_v3(hass: HomeAssistant) -> None: - """Test we handle a good wallet currency option.""" - +async def test_reauth_flow_invalid_auth(hass: HomeAssistant) -> None: + """Test reauth flow with invalid credentials.""" with ( - patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( "coinbase.rest.RESTClient.get_portfolios", return_value=mock_get_portfolios(), ), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.rest.RESTBase.get", + "coinbase.rest.RESTClient.get", return_value={"data": mock_get_exchange_rates()}, ), - patch( - "homeassistant.components.coinbase.update_listener" - ) as mock_update_listener, ): - config_entry = await init_mock_coinbase_v3(hass) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - result2 = await hass.config_entries.options.async_configure( + config_entry = await init_mock_coinbase(hass) + + # Start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + + # Test invalid auth during reauth + api_auth_error_key = HTTPError("invalid api key") + with patch( + "coinbase.rest.RESTClient.get_portfolios", + side_effect=api_auth_error_key, + ): + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_CURRENCIES: [GOOD_CURRENCY], - CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE], - CONF_EXCHANGE_PRECISION: 5, + { + CONF_API_KEY: "bad_key", + CONF_API_TOKEN: "bad_secret", }, ) - assert result2["type"] is FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - assert len(mock_update_listener.mock_calls) == 1 + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "reauth_confirm" + assert result2["errors"] == {"base": "invalid_auth_key"} diff --git a/tests/components/coinbase/test_diagnostics.py b/tests/components/coinbase/test_diagnostics.py index 98936f47e48..5e708756d80 100644 --- a/tests/components/coinbase/test_diagnostics.py +++ b/tests/components/coinbase/test_diagnostics.py @@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant from .common import ( init_mock_coinbase, - mock_get_current_user, mock_get_exchange_rates, - mocked_get_accounts, + mock_get_portfolios, + mocked_get_accounts_v3, ) from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -27,13 +27,13 @@ async def test_entry_diagnostics( with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), ): config_entry = await init_mock_coinbase(hass) diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py index 99b6bb4a9bd..7705a4d8e81 100644 --- a/tests/components/coinbase/test_init.py +++ b/tests/components/coinbase/test_init.py @@ -2,6 +2,9 @@ from unittest.mock import patch +import pytest + +from homeassistant.components.coinbase import create_and_update_instance from homeassistant.components.coinbase.const import ( API_TYPE_VAULT, CONF_CURRENCIES, @@ -9,14 +12,16 @@ from homeassistant.components.coinbase.const import ( DOMAIN, ) from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import entity_registry as er from .common import ( init_mock_coinbase, - mock_get_current_user, mock_get_exchange_rates, - mocked_get_accounts, + mock_get_portfolios, + mocked_get_accounts_v3, ) from .const import ( GOOD_CURRENCY, @@ -30,16 +35,16 @@ async def test_unload_entry(hass: HomeAssistant) -> None: """Test successful unload of entry.""" with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), patch( - "coinbase.wallet.client.Client.get_accounts", - new=mocked_get_accounts, + "coinbase.rest.RESTClient.get_accounts", + new=mocked_get_accounts_v3, ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": {"rates": {}}}, ), ): entry = await init_mock_coinbase(hass) @@ -61,13 +66,13 @@ async def test_option_updates( with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), ): config_entry = await init_mock_coinbase(hass) @@ -141,13 +146,13 @@ async def test_ignore_vaults_wallets( with ( patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), ), - patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, ), ): config_entry = await init_mock_coinbase(hass, currencies=[GOOD_CURRENCY]) @@ -159,3 +164,54 @@ async def test_ignore_vaults_wallets( assert len(entities) == 1 entity = entities[0] assert API_TYPE_VAULT not in entity.original_name.lower() + + +async def test_v2_api_credentials_trigger_reauth(hass: HomeAssistant) -> None: + """Test that v2 API credentials trigger a reauth flow.""" + + config_entry_data = { + CONF_API_KEY: "v2_api_key_legacy_format", + CONF_API_TOKEN: "v2_api_secret", + } + + class MockConfigEntry: + def __init__(self, data) -> None: + self.data = data + self.options = {} + + entry = MockConfigEntry(config_entry_data) + + with pytest.raises(ConfigEntryAuthFailed) as exc_info: + create_and_update_instance(entry) + + assert "deprecated v2 API" in str(exc_info.value) + + +async def test_v3_api_credentials_work(hass: HomeAssistant) -> None: + """Test that v3 API credentials with 'organizations' don't trigger reauth.""" + + config_entry_data = { + CONF_API_KEY: "organizations_v3_api_key", + CONF_API_TOKEN: "v3_api_secret", + } + + class MockConfigEntry: + def __init__(self, data) -> None: + self.data = data + self.options = {} + + entry = MockConfigEntry(config_entry_data) + + with ( + patch( + "coinbase.rest.RESTClient.get_portfolios", + return_value=mock_get_portfolios(), + ), + patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3), + patch( + "coinbase.rest.RESTClient.get", + return_value={"data": mock_get_exchange_rates()}, + ), + ): + instance = create_and_update_instance(entry) + assert instance is not None diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index c6e82976bf1..8f89549944c 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -625,7 +625,9 @@ async def test_two_step_flow(hass: HomeAssistant, client: TestClient) -> None: "type": "form", "handler": "test", "step_id": "account", - "data_schema": [{"name": "user_title", "type": "string"}], + "data_schema": [ + {"name": "user_title", "required": False, "type": "string"} + ], "description_placeholders": None, "errors": None, "last_step": None, @@ -712,7 +714,9 @@ async def test_continue_flow_unauth( "type": "form", "handler": "test", "step_id": "account", - "data_schema": [{"name": "user_title", "type": "string"}], + "data_schema": [ + {"name": "user_title", "required": False, "type": "string"} + ], "description_placeholders": None, "errors": None, "last_step": None, @@ -1272,7 +1276,7 @@ async def test_two_step_options_flow(hass: HomeAssistant, client: TestClient) -> "type": "form", "handler": "test1", "step_id": "finish", - "data_schema": [{"name": "enabled", "type": "boolean"}], + "data_schema": [{"name": "enabled", "required": False, "type": "boolean"}], "description_placeholders": None, "errors": None, "last_step": None, @@ -1581,7 +1585,7 @@ async def test_subentry_flow_abort_duplicate(hass: HomeAssistant, client) -> Non "type": "form", "handler": ["test1", "test"], "step_id": "finish", - "data_schema": [{"name": "enabled", "type": "boolean"}], + "data_schema": [{"name": "enabled", "required": False, "type": "boolean"}], "description_placeholders": None, "errors": None, "last_step": None, @@ -1749,7 +1753,7 @@ async def test_two_step_subentry_flow(hass: HomeAssistant, client) -> None: data = await resp.json() flow_id = data["flow_id"] expected_data = { - "data_schema": [{"name": "enabled", "type": "boolean"}], + "data_schema": [{"name": "enabled", "required": False, "type": "boolean"}], "description_placeholders": None, "errors": None, "flow_id": flow_id, diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index 8dfe879ee2b..19d8434fc5a 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -73,4 +73,8 @@ async def sl_setup(hass: HomeAssistant): async def init_components(hass: HomeAssistant): """Initialize relevant components with empty configs.""" assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "conversation", {conversation.DOMAIN: {}}) + + # Disable fuzzy matching by default for tests + agent = hass.data[conversation.DATA_DEFAULT_ENTITY] + agent.fuzzy_matching = False diff --git a/tests/components/conversation/snapshots/test_chat_log.ambr b/tests/components/conversation/snapshots/test_chat_log.ambr index ff8ebf724cd..787009ba614 100644 --- a/tests/components/conversation/snapshots/test_chat_log.ambr +++ b/tests/components/conversation/snapshots/test_chat_log.ambr @@ -3,12 +3,55 @@ list([ ]) # --- +# name: test_add_delta_content_stream[deltas10] + list([ + dict({ + 'agent_id': 'mock-agent-id', + 'content': None, + 'native': object( + ), + 'role': 'assistant', + 'thinking_content': None, + 'tool_calls': None, + }), + ]) +# --- +# name: test_add_delta_content_stream[deltas11] + list([ + dict({ + 'agent_id': 'mock-agent-id', + 'content': 'Test', + 'native': None, + 'role': 'assistant', + 'thinking_content': None, + 'tool_calls': list([ + dict({ + 'external': True, + 'id': 'mock-tool-call-id', + 'tool_args': dict({ + 'param1': 'Test Param 1', + }), + 'tool_name': 'test_tool', + }), + ]), + }), + dict({ + 'agent_id': 'mock-agent-id', + 'role': 'tool_result', + 'tool_call_id': 'mock-tool-call-id', + 'tool_name': 'test_tool', + 'tool_result': 'Test Result', + }), + ]) +# --- # name: test_add_delta_content_stream[deltas1] list([ dict({ 'agent_id': 'mock-agent-id', 'content': 'Test', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) @@ -18,13 +61,17 @@ dict({ 'agent_id': 'mock-agent-id', 'content': 'Test', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), dict({ 'agent_id': 'mock-agent-id', 'content': 'Test 2', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) @@ -34,9 +81,12 @@ dict({ 'agent_id': 'mock-agent-id', 'content': None, + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'mock-tool-call-id', 'tool_args': dict({ 'param1': 'Test Param 1', @@ -59,9 +109,12 @@ dict({ 'agent_id': 'mock-agent-id', 'content': 'Test', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'mock-tool-call-id', 'tool_args': dict({ 'param1': 'Test Param 1', @@ -84,9 +137,12 @@ dict({ 'agent_id': 'mock-agent-id', 'content': 'Test', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'mock-tool-call-id', 'tool_args': dict({ 'param1': 'Test Param 1', @@ -105,7 +161,9 @@ dict({ 'agent_id': 'mock-agent-id', 'content': 'Test 2', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) @@ -115,9 +173,12 @@ dict({ 'agent_id': 'mock-agent-id', 'content': None, + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'mock-tool-call-id', 'tool_args': dict({ 'param1': 'Test Param 1', @@ -125,6 +186,7 @@ 'tool_name': 'test_tool', }), dict({ + 'external': False, 'id': 'mock-tool-call-id-2', 'tool_args': dict({ 'param1': 'Test Param 2', @@ -149,6 +211,45 @@ }), ]) # --- +# name: test_add_delta_content_stream[deltas7] + list([ + dict({ + 'agent_id': 'mock-agent-id', + 'content': None, + 'native': None, + 'role': 'assistant', + 'thinking_content': 'Test Thinking', + 'tool_calls': None, + }), + ]) +# --- +# name: test_add_delta_content_stream[deltas8] + list([ + dict({ + 'agent_id': 'mock-agent-id', + 'content': 'Test', + 'native': None, + 'role': 'assistant', + 'thinking_content': 'Test Thinking', + 'tool_calls': None, + }), + ]) +# --- +# name: test_add_delta_content_stream[deltas9] + list([ + dict({ + 'agent_id': 'mock-agent-id', + 'content': None, + 'native': dict({ + 'type': 'test', + 'value': 'Test Native', + }), + 'role': 'assistant', + 'thinking_content': None, + 'tool_calls': None, + }), + ]) +# --- # name: test_template_error dict({ 'continue_conversation': False, diff --git a/tests/components/conversation/snapshots/test_http.ambr b/tests/components/conversation/snapshots/test_http.ambr index 8f68274d37f..8b8ed6fa71c 100644 --- a/tests/components/conversation/snapshots/test_http.ambr +++ b/tests/components/conversation/snapshots/test_http.ambr @@ -464,6 +464,7 @@ 'value': 'my cool light', }), }), + 'fuzzy_match': False, 'intent': dict({ 'name': 'HassTurnOn', }), @@ -472,7 +473,6 @@ 'slots': dict({ 'name': 'my cool light', }), - 'source': 'builtin', 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -489,6 +489,7 @@ 'value': 'my cool light', }), }), + 'fuzzy_match': False, 'intent': dict({ 'name': 'HassTurnOff', }), @@ -497,7 +498,6 @@ 'slots': dict({ 'name': 'my cool light', }), - 'source': 'builtin', 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -519,6 +519,7 @@ 'value': 'light', }), }), + 'fuzzy_match': False, 'intent': dict({ 'name': 'HassTurnOn', }), @@ -528,7 +529,6 @@ 'area': 'kitchen', 'domain': 'light', }), - 'source': 'builtin', 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -555,6 +555,7 @@ 'value': 'on', }), }), + 'fuzzy_match': False, 'intent': dict({ 'name': 'HassGetState', }), @@ -565,7 +566,6 @@ 'domain': 'lights', 'state': 'on', }), - 'source': 'builtin', 'targets': dict({ 'light.kitchen': dict({ 'matched': False, @@ -590,6 +590,7 @@ }), }), 'file': 'en/beer.yaml', + 'fuzzy_match': False, 'intent': dict({ 'name': 'OrderBeer', }), @@ -630,6 +631,7 @@ 'value': 'test light', }), }), + 'fuzzy_match': False, 'intent': dict({ 'name': 'HassLightSet', }), @@ -639,7 +641,6 @@ 'brightness': '100', 'name': 'test light', }), - 'source': 'builtin', 'targets': dict({ 'light.demo_1234': dict({ 'matched': True, @@ -662,6 +663,7 @@ 'value': 'test light', }), }), + 'fuzzy_match': False, 'intent': dict({ 'name': 'HassLightSet', }), @@ -670,7 +672,6 @@ 'slots': dict({ 'name': 'test light', }), - 'source': 'builtin', 'targets': dict({ }), 'unmatched_slots': dict({ diff --git a/tests/components/conversation/test_chat_log.py b/tests/components/conversation/test_chat_log.py index 811c045dd70..e851512b36e 100644 --- a/tests/components/conversation/test_chat_log.py +++ b/tests/components/conversation/test_chat_log.py @@ -517,6 +517,48 @@ async def test_tool_call_exception( ] }, ], + # With thinking content + [ + {"role": "assistant"}, + {"thinking_content": "Test Thinking"}, + ], + # With content and thinking content + [ + {"role": "assistant"}, + {"content": "Test"}, + {"thinking_content": "Test Thinking"}, + ], + # With native content + [ + {"role": "assistant"}, + {"native": {"type": "test", "value": "Test Native"}}, + ], + # With native object content + [ + {"role": "assistant"}, + {"native": object()}, + ], + # With external tool calls + [ + {"role": "assistant"}, + {"content": "Test"}, + { + "tool_calls": [ + llm.ToolInput( + id="mock-tool-call-id", + tool_name="test_tool", + tool_args={"param1": "Test Param 1"}, + external=True, + ) + ] + }, + { + "role": "tool_result", + "tool_call_id": "mock-tool-call-id", + "tool_name": "test_tool", + "tool_result": "Test Result", + }, + ], ], ) async def test_add_delta_content_stream( @@ -547,7 +589,9 @@ async def test_add_delta_content_stream( """Yield deltas.""" for d in deltas: yield d - expected_delta.append(d) + if filtered_delta := {k: v for k, v in d.items() if k != "native"}: + if filtered_delta.get("role") != "tool_result": + expected_delta.append(filtered_delta) captured_deltas = [] @@ -634,6 +678,20 @@ async def test_add_delta_content_stream_errors( ): pass + # Second native content + with pytest.raises(RuntimeError): + async for _tool_result_content in chat_log.async_add_delta_content_stream( + "mock-agent-id", + stream( + [ + {"role": "assistant"}, + {"native": "Test Native"}, + {"native": "Test Native 2"}, + ] + ), + ): + pass + async def test_chat_log_reuse( hass: HomeAssistant, diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index f075f267111..7c5e897d86c 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -25,7 +25,12 @@ from homeassistant.components.intent import ( TimerInfo, async_register_timer_handler, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ( + ATTR_SUPPORTED_COLOR_MODES, + DOMAIN as LIGHT_DOMAIN, + ColorMode, + intent as light_intent, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -81,6 +86,10 @@ async def init_components(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "conversation", {}) assert await async_setup_component(hass, "intent", {}) + # Disable fuzzy matching by default for tests + agent = hass.data[DATA_DEFAULT_ENTITY] + agent.fuzzy_matching = False + @pytest.mark.parametrize( "er_kwargs", @@ -3287,3 +3296,97 @@ async def test_language_with_alternative_code( assert call.domain == LIGHT_DOMAIN assert call.service == "turn_on" assert call.data == {"entity_id": [entity_id]} + + +@pytest.mark.parametrize("fuzzy_matching", [True, False]) +@pytest.mark.parametrize( + ("sentence", "intent_type", "slots"), + [ + ("time", "HassGetCurrentTime", {}), + ("how about my timers", "HassTimerStatus", {}), + ( + "the office needs more blue", + "HassLightSet", + {"area": "office", "color": "blue"}, + ), + ( + "50% office light", + "HassLightSet", + {"name": "office light", "brightness": "50%"}, + ), + ], +) +async def test_fuzzy_matching( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + fuzzy_matching: bool, + sentence: str, + intent_type: str, + slots: dict[str, Any], +) -> None: + """Test fuzzy vs. non-fuzzy matching on some English sentences.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) + await light_intent.async_setup_intents(hass) + + agent = hass.data[DATA_DEFAULT_ENTITY] + agent.fuzzy_matching = fuzzy_matching + + area_office = area_registry.async_get_or_create("office_id") + area_office = area_registry.async_update(area_office.id, name="office") + + entry = MockConfigEntry() + entry.add_to_hass(hass) + office_satellite = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "id-1234")}, + ) + device_registry.async_update_device(office_satellite.id, area_id=area_office.id) + + office_light = entity_registry.async_get_or_create("light", "demo", "1234") + office_light = entity_registry.async_update_entity( + office_light.entity_id, area_id=area_office.id + ) + hass.states.async_set( + office_light.entity_id, + "on", + attributes={ + ATTR_FRIENDLY_NAME: "office light", + ATTR_SUPPORTED_COLOR_MODES: [ColorMode.BRIGHTNESS, ColorMode.RGB], + }, + ) + _on_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") + + result = await conversation.async_converse( + hass, + sentence, + None, + Context(), + language="en", + device_id=office_satellite.id, + ) + response = result.response + + if not fuzzy_matching: + # Should not match + assert response.response_type == intent.IntentResponseType.ERROR + return + + assert response.response_type in ( + intent.IntentResponseType.ACTION_DONE, + intent.IntentResponseType.QUERY_ANSWER, + ) + assert response.intent is not None + assert response.intent.intent_type == intent_type + + # Verify slot texts match + actual_slots = { + slot_name: slot_value["text"] + for slot_name, slot_value in response.intent.slots.items() + if slot_name != "preferred_area_id" # context area + } + assert actual_slots == slots diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index db9e75bcaef..438e5de751d 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -260,6 +260,7 @@ async def test_get_action_capabilities_set_pos( { "name": "position", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -310,6 +311,7 @@ async def test_get_action_capabilities_set_tilt_pos( { "name": "position", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index aa5f150172c..5bd02120585 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -254,6 +254,7 @@ async def test_get_condition_capabilities_set_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -262,6 +263,7 @@ async def test_get_condition_capabilities_set_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, @@ -311,6 +313,7 @@ async def test_get_condition_capabilities_set_tilt_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -319,6 +322,7 @@ async def test_get_condition_capabilities_set_tilt_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 7901baaa3b8..1a6b50b2935 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -192,7 +192,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -230,7 +235,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -262,6 +272,7 @@ async def test_get_trigger_capabilities_set_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -270,6 +281,7 @@ async def test_get_trigger_capabilities_set_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, @@ -293,6 +305,7 @@ async def test_get_trigger_capabilities_set_pos( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ] @@ -326,6 +339,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -334,6 +348,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, @@ -357,6 +372,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ] diff --git a/tests/components/deconz/snapshots/test_sensor.ambr b/tests/components/deconz/snapshots/test_sensor.ambr index 04f93738b18..4a6bc43043b 100644 --- a/tests/components/deconz/snapshots/test_sensor.ambr +++ b/tests/components/deconz/snapshots/test_sensor.ambr @@ -829,7 +829,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-042a-particulate_matter_pm2_5', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[config_entry_options0-sensor_payload14-expected14][sensor.starkvind_airpurifier_pm25-state] @@ -838,7 +838,7 @@ 'device_class': 'pm25', 'friendly_name': 'STARKVIND AirPurifier PM25', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.starkvind_airpurifier_pm25', @@ -1377,7 +1377,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_ch2o-state] @@ -1386,7 +1386,7 @@ 'device_class': 'volatile_organic_compounds', 'friendly_name': 'AirQuality 1 CH2O', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airquality_1_ch2o', @@ -1483,7 +1483,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_pm25-state] @@ -1492,7 +1492,7 @@ 'device_class': 'pm25', 'friendly_name': 'AirQuality 1 PM25', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airquality_1_pm25', @@ -1699,7 +1699,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_ch2o-state] @@ -1708,7 +1708,7 @@ 'device_class': 'volatile_organic_compounds', 'friendly_name': 'AirQuality 1 CH2O', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airquality_1_ch2o', @@ -1805,7 +1805,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_pm25-state] @@ -1814,7 +1814,7 @@ 'device_class': 'pm25', 'friendly_name': 'AirQuality 1 PM25', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airquality_1_pm25', @@ -1910,7 +1910,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_ch2o-state] @@ -1919,7 +1919,7 @@ 'device_class': 'volatile_organic_compounds', 'friendly_name': 'AirQuality 1 CH2O', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airquality_1_ch2o', @@ -2016,7 +2016,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_pm25-state] @@ -2025,7 +2025,7 @@ 'device_class': 'pm25', 'friendly_name': 'AirQuality 1 PM25', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airquality_1_pm25', diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 7487a4c13e3..c22b28ae799 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -55,7 +55,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION, _make_key from homeassistant.setup import async_setup_component -from tests.typing import ClientSessionGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator TEST_ENTITY_ID = "media_player.walkman" @@ -563,3 +563,32 @@ async def test_grouping(hass: HomeAssistant) -> None: ) state = hass.states.get(walkman) assert state.attributes.get(ATTR_GROUP_MEMBERS) == [] + + +async def test_browse( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the media player browse.""" + entity = "media_player.browse" + + await async_setup_component(hass, "media_source", {"media_source": {}}) + assert await async_setup_component( + hass, MP_DOMAIN, {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + websocket_client = await hass_ws_client(hass) + await websocket_client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": entity, + } + ) + + msg = await websocket_client.receive_json() + assert msg["success"] + assert msg["result"]["title"] == "media" + assert msg["result"]["media_class"] == "directory" + assert len(msg["result"]["children"]) diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 440df495995..5e2d9446cdc 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -68,8 +68,14 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: @pytest.mark.parametrize("platform", ["sensor"]) -async def test_options(hass: HomeAssistant, platform) -> None: - """Test reconfiguring.""" +@pytest.mark.parametrize( + ("unit_prefix_entry", "unit_prefix_used"), + [("k", "k"), ("\u00b5", "\u03bc"), ("\u03bc", "\u03bc")], +) +async def test_options( + hass: HomeAssistant, platform, unit_prefix_entry: str, unit_prefix_used: str +) -> None: + """Test reconfiguring and migrated unit prefix.""" # Setup the config entry config_entry = MockConfigEntry( data={}, @@ -79,7 +85,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "round": 1.0, "source": "sensor.input", "time_window": {"seconds": 0.0}, - "unit_prefix": "k", + "unit_prefix": unit_prefix_entry, "unit_time": "min", "max_sub_interval": {"seconds": 30}, }, @@ -99,7 +105,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: schema = result["data_schema"].schema assert get_schema_suggested_value(schema, "round") == 1.0 assert get_schema_suggested_value(schema, "time_window") == {"seconds": 0.0} - assert get_schema_suggested_value(schema, "unit_prefix") == "k" + assert get_schema_suggested_value(schema, "unit_prefix") == unit_prefix_used assert get_schema_suggested_value(schema, "unit_time") == "min" source = schema["source"] diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index abe90e72b56..005e6ec91d9 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -519,7 +519,7 @@ async def test_migration_1_1(hass: HomeAssistant, unit_prefix, expect_prefix) -> assert config_entry.options.get("unit_prefix") == expect_prefix assert config_entry.version == 1 - assert config_entry.minor_version == 3 + assert config_entry.minor_version == 4 async def test_migration_1_2( @@ -570,7 +570,44 @@ async def test_migration_1_2( assert derivative_entity_entry.device_id == sensor_entity_entry.device_id assert derivative_config_entry.version == 1 - assert derivative_config_entry.minor_version == 3 + assert derivative_config_entry.minor_version == 4 + + +@pytest.mark.parametrize( + ("unit_prefix", "expect_prefix"), + [ + ({"unit_prefix": "\u00b5"}, "\u03bc"), + ({"unit_prefix": "\u03bc"}, "\u03bc"), + ], +) +async def test_migration_1_4(hass: HomeAssistant, unit_prefix, expect_prefix) -> None: + """Test migration from v1.4 migrates to Greek Mu char" unit_prefix.""" + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My derivative", + "round": 1.0, + "source": "sensor.power", + "time_window": {"seconds": 0.0}, + **unit_prefix, + "unit_time": "min", + }, + title="My derivative", + version=1, + minor_version=1, + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.options["unit_time"] == "min" + assert config_entry.options.get("unit_prefix") == expect_prefix + + assert config_entry.version == 1 + assert config_entry.minor_version == 4 async def test_migration_from_future_version( diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 94625746b05..456202a63a4 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -293,7 +293,9 @@ async def test_websocket_get_action_capabilities( ) expected_capabilities = { "turn_on": { - "extra_fields": [{"type": "string", "name": "code", "optional": True}] + "extra_fields": [ + {"type": "string", "name": "code", "optional": True, "required": False} + ] }, "turn_off": {"extra_fields": []}, "toggle": {"extra_fields": []}, @@ -452,7 +454,12 @@ async def test_websocket_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -745,7 +752,12 @@ async def test_websocket_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/emoncms_history/__init__.py b/tests/components/emoncms_history/__init__.py new file mode 100644 index 00000000000..0c60d27655b --- /dev/null +++ b/tests/components/emoncms_history/__init__.py @@ -0,0 +1 @@ +"""Tests for emoncms_history component.""" diff --git a/tests/components/emoncms_history/test_init.py b/tests/components/emoncms_history/test_init.py new file mode 100644 index 00000000000..c62252750b5 --- /dev/null +++ b/tests/components/emoncms_history/test_init.py @@ -0,0 +1,125 @@ +"""The tests for the emoncms_history init.""" + +from collections.abc import AsyncGenerator +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +import aiohttp +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.const import CONF_API_KEY, CONF_URL, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_setup_valid_config(hass: HomeAssistant) -> None: + """Test setting up the emoncms_history component with valid configuration.""" + config = { + "emoncms_history": { + CONF_API_KEY: "dummy", + CONF_URL: "https://emoncms.example", + "inputnode": 42, + "whitelist": ["sensor.temp"], + } + } + # Simulate a sensor + hass.states.async_set("sensor.temp", "23.4", {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + assert await async_setup_component(hass, "emoncms_history", config) + await hass.async_block_till_done() + + +async def test_setup_missing_config(hass: HomeAssistant) -> None: + """Test setting up the emoncms_history component with missing configuration.""" + config = {"emoncms_history": {"api_key": "dummy"}} + success = await async_setup_component(hass, "emoncms_history", config) + assert not success + + +@pytest.fixture +async def emoncms_client() -> AsyncGenerator[AsyncMock]: + """Mock pyemoncms client with successful responses.""" + with patch( + "homeassistant.components.emoncms_history.EmoncmsClient", autospec=True + ) as mock_client: + client = mock_client.return_value + client.async_input_post.return_value = '{"success": true}' + yield client + + +async def test_emoncms_send_data( + hass: HomeAssistant, + emoncms_client: AsyncMock, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sending data to Emoncms with and without success.""" + + config = { + "emoncms_history": { + "api_key": "dummy", + "url": "http://fake-url", + "inputnode": 42, + "whitelist": ["sensor.temp"], + } + } + + assert await async_setup_component(hass, "emoncms_history", config) + await hass.async_block_till_done() + + for state in None, "", STATE_UNAVAILABLE, STATE_UNKNOWN: + hass.states.async_set("sensor.temp", state, {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + assert emoncms_client.async_input_post.call_args is None + + hass.states.async_set("sensor.temp", "not_a_number", {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + emoncms_client.async_input_post.assert_not_called() + + hass.states.async_set("sensor.temp", "23.4", {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + emoncms_client.async_input_post.assert_called_once() + assert emoncms_client.async_input_post.return_value == '{"success": true}' + + _, kwargs = emoncms_client.async_input_post.call_args + assert kwargs["data"] == {"sensor.temp": 23.4} + assert kwargs["node"] == "42" + + emoncms_client.async_input_post.side_effect = aiohttp.ClientError( + "Connection refused" + ) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + assert any( + "Network error when sending data to Emoncms" in message + for message in caplog.text.splitlines() + ) + + emoncms_client.async_input_post.side_effect = ValueError("Invalid value format") + + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + assert any( + "Value error when preparing data for Emoncms" in message + for message in caplog.text.splitlines() + ) diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 7ad15f85ac2..9e94dab5a4c 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -9,6 +9,8 @@ import multidict from pyenphase import ( EnvoyACBPower, EnvoyBatteryAggregate, + EnvoyC6CC, + EnvoyCollar, EnvoyData, EnvoyEncharge, EnvoyEnchargeAggregate, @@ -260,6 +262,10 @@ def _load_json_2_encharge_enpower_data( ) if item := json_fixture["data"].get("battery_aggregate"): mocked_data.battery_aggregate = EnvoyBatteryAggregate(**item) + if item := json_fixture["data"].get("collar"): + mocked_data.collar = EnvoyCollar(**item) + if item := json_fixture["data"].get("c6cc"): + mocked_data.c6cc = EnvoyC6CC(**item) def _load_json_2_raw_data(mocked_data: EnvoyData, json_fixture: dict[str, Any]) -> None: diff --git a/tests/components/enphase_envoy/fixtures/envoy_metered_batt_relay.json b/tests/components/enphase_envoy/fixtures/envoy_metered_batt_relay.json index 73af5af0e5d..e8e0fd8ac85 100644 --- a/tests/components/enphase_envoy/fixtures/envoy_metered_batt_relay.json +++ b/tests/components/enphase_envoy/fixtures/envoy_metered_batt_relay.json @@ -407,6 +407,35 @@ "type": "NONE" } }, + "collar": { + "admin_state": 88, + "admin_state_str": "ENCMN_MDE_ON_GRID", + "firmware_loaded_date": 1752939759, + "firmware_version": "3.0.6-D0", + "installed_date": 1752939759, + "last_report_date": 1752939759, + "communicating": true, + "mid_state": "close", + "grid_state": "on_grid", + "part_number": "865-00400-r22", + "serial_number": "482520020939", + "temperature": 42, + "temperature_unit": "C", + "control_error": 0, + "collar_state": "Installed" + }, + "c6cc": { + "admin_state": 82, + "admin_state_str": "ENCMN_C6_CC_READY", + "firmware_loaded_date": 1752945451, + "firmware_version": "0.1.20-D1", + "installed_date": 1752945451, + "last_report_date": 1752945451, + "communicating": true, + "part_number": "800-02403-r08", + "serial_number": "482523040549", + "dmir_version": "0.1.20-D1" + }, "inverters": { "1": { "serial_number": "1", diff --git a/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr index bbf35621c6c..18e7a9c9008 100644 --- a/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr @@ -96,6 +96,104 @@ 'state': 'on', }) # --- +# name: test_binary_sensor[envoy_metered_batt_relay][binary_sensor.c6_combiner_482523040549_communicating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.c6_combiner_482523040549_communicating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Communicating', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'communicating', + 'unique_id': '482523040549_communicating', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[envoy_metered_batt_relay][binary_sensor.c6_combiner_482523040549_communicating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'C6 Combiner 482523040549 Communicating', + }), + 'context': , + 'entity_id': 'binary_sensor.c6_combiner_482523040549_communicating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[envoy_metered_batt_relay][binary_sensor.collar_482520020939_communicating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.collar_482520020939_communicating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Communicating', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'communicating', + 'unique_id': '482520020939_communicating', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[envoy_metered_batt_relay][binary_sensor.collar_482520020939_communicating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Collar 482520020939 Communicating', + }), + 'context': , + 'entity_id': 'binary_sensor.collar_482520020939_communicating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensor[envoy_metered_batt_relay][binary_sensor.encharge_123456_communicating-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/enphase_envoy/snapshots/test_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_sensor.ambr index 4a9563ce906..00cb30fce09 100644 --- a/tests/components/enphase_envoy/snapshots/test_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_sensor.ambr @@ -13947,6 +13947,253 @@ 'state': 'unknown', }) # --- +# name: test_sensor[envoy_metered_batt_relay][sensor.c6_combiner_482523040549_last_reported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.c6_combiner_482523040549_last_reported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last reported', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_reported', + 'unique_id': '482523040549_last_reported', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.c6_combiner_482523040549_last_reported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'C6 Combiner 482523040549 Last reported', + }), + 'context': , + 'entity_id': 'sensor.c6_combiner_482523040549_last_reported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-07-19T17:17:31+00:00', + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_grid_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.collar_482520020939_grid_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Grid status', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grid_status', + 'unique_id': '482520020939_grid_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_grid_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Collar 482520020939 Grid status', + }), + 'context': , + 'entity_id': 'sensor.collar_482520020939_grid_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on_grid', + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_last_reported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.collar_482520020939_last_reported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last reported', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_reported', + 'unique_id': '482520020939_last_reported', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_last_reported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Collar 482520020939 Last reported', + }), + 'context': , + 'entity_id': 'sensor.collar_482520020939_last_reported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-07-19T15:42:39+00:00', + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_mid_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.collar_482520020939_mid_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'MID state', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mid_state', + 'unique_id': '482520020939_mid_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_mid_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Collar 482520020939 MID state', + }), + 'context': , + 'entity_id': 'sensor.collar_482520020939_mid_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'close', + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.collar_482520020939_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '482520020939_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[envoy_metered_batt_relay][sensor.collar_482520020939_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Collar 482520020939 Temperature', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.collar_482520020939_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42', + }) +# --- # name: test_sensor[envoy_metered_batt_relay][sensor.encharge_123456_apparent_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 9de97bac3eb..f9383d3b4f7 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -517,9 +517,30 @@ async def _mock_generic_device_entry( mock_client.list_entities_services = AsyncMock( return_value=mock_list_entities_services ) - mock_client.subscribe_states = _subscribe_states - mock_client.subscribe_service_calls = _subscribe_service_calls - mock_client.subscribe_home_assistant_states = _subscribe_home_assistant_states + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(mock_device.device_info, *mock_list_entities_services) + ) + + def _subscribe_home_assistant_states_and_services( + *, + on_state: Callable[[EntityState], None], + on_service_call: Callable[[HomeassistantServiceCall], None], + on_state_sub: Callable[[str, str | None], None], + on_state_request: Callable[[str, str | None], None], + ) -> None: + """Subscribe to states and service calls.""" + mock_device.set_state_callback(on_state) + mock_device.set_service_call_callback(on_service_call) + mock_device.set_home_assistant_state_subscription_callback( + on_state_sub, on_state_request + ) + # Set the initial states + for state in states: + on_state(state) + + mock_client.subscribe_home_assistant_states_and_services = ( + _subscribe_home_assistant_states_and_services + ) mock_client.subscribe_logs = _subscribe_logs try_connect_done = Event() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 0fda7714dd0..1bedc6d79f8 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1045,8 +1045,11 @@ async def test_encryption_key_valid_psk( assert result["step_id"] == "encryption_key" assert result["description_placeholders"] == {"name": "ESPHome"} - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=False, name="test") + device_info = DeviceInfo(uses_password=False, name="test") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} @@ -1363,10 +1366,13 @@ async def test_reauth_confirm_invalid( assert result["errors"] assert result["errors"]["base"] == "invalid_psk" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test", mac_address="11:22:33:44:55:aa" - ) + device_info = DeviceInfo( + uses_password=False, name="test", mac_address="11:22:33:44:55:aa" + ) + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} @@ -1404,10 +1410,13 @@ async def test_reauth_confirm_invalid_with_unique_id( assert result["errors"] assert result["errors"]["base"] == "invalid_psk" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test", mac_address="11:22:33:44:55:aa" - ) + device_info = DeviceInfo( + uses_password=False, name="test", mac_address="11:22:33:44:55:aa" + ) + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} @@ -1460,8 +1469,11 @@ async def test_discovery_dhcp_updates_host( unique_id="11:22:33:44:55:aa", ) entry.add_to_hass(hass) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(name="test8266", mac_address="1122334455aa") + device_info = DeviceInfo(name="test8266", mac_address="1122334455aa") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) service_info = DhcpServiceInfo( @@ -1496,8 +1508,11 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac( unique_id="11:22:33:44:55:aa", ) entry.add_to_hass(hass) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(name="test8266", mac_address="1122334455ff") + device_info = DeviceInfo(name="test8266", mac_address="1122334455ff") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) service_info = DhcpServiceInfo( @@ -1602,7 +1617,12 @@ async def test_discovery_dhcp_no_changes( ) entry.add_to_hass(hass) - mock_client.device_info = AsyncMock(return_value=DeviceInfo(name="test8266")) + device_info = DeviceInfo(name="test8266") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) + ) service_info = DhcpServiceInfo( ip="192.168.43.183", @@ -2034,12 +2054,15 @@ async def test_user_flow_name_conflict_migrate( unique_id="11:22:33:44:55:cc", ) existing_entry.add_to_hass(hass) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, - name="test", - mac_address="11:22:33:44:55:AA", - ) + device_info = DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ) + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) result = await hass.config_entries.flow.async_init( @@ -2084,12 +2107,15 @@ async def test_user_flow_name_conflict_overwrite( unique_id="11:22:33:44:55:cc", ) existing_entry.add_to_hass(hass) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, - name="test", - mac_address="11:22:33:44:55:AA", - ) + device_info = DeviceInfo( + uses_password=False, + name="test", + mac_address="11:22:33:44:55:AA", + ) + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 9b3c08bb77d..8f2d7c33575 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -214,6 +214,9 @@ async def test_entities_removed_after_reload( mock_device.client.list_entities_services = AsyncMock( return_value=(entity_info, []) ) + mock_device.client.device_info_and_list_entities = AsyncMock( + return_value=(mock_device.device_info, entity_info, []) + ) assert await hass.config_entries.async_setup(entry.entry_id) on_future = hass.loop.create_future() @@ -677,6 +680,13 @@ async def test_deep_sleep_added_after_setup( **{**asdict(mock_device.device_info), "has_deep_sleep": True} ) mock_device.client.device_info = AsyncMock(return_value=new_device_info) + mock_device.client.device_info_and_list_entities = AsyncMock( + return_value=( + new_device_info, + mock_device.client.list_entities_services.return_value[0], + mock_device.client.list_entities_services.return_value[1], + ) + ) mock_device.device_info = new_device_info await mock_device.mock_connect() @@ -952,6 +962,9 @@ async def test_entity_switches_between_devices( mock_client.list_entities_services = AsyncMock( return_value=(updated_entity_info, []) ) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, updated_entity_info, []) + ) # Trigger a reconnect to simulate the entity info update await device.mock_disconnect(expected_disconnect=False) await device.mock_connect() @@ -979,6 +992,9 @@ async def test_entity_switches_between_devices( mock_client.list_entities_services = AsyncMock( return_value=(updated_entity_info, []) ) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, updated_entity_info, []) + ) await device.mock_disconnect(expected_disconnect=False) await device.mock_connect() @@ -1005,6 +1021,9 @@ async def test_entity_switches_between_devices( mock_client.list_entities_services = AsyncMock( return_value=(updated_entity_info, []) ) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, updated_entity_info, []) + ) await device.mock_disconnect(expected_disconnect=False) await device.mock_connect() @@ -1228,6 +1247,9 @@ async def test_unique_id_migration_when_entity_moves_between_devices( # Update the entity info by changing what the mock returns mock_client.list_entities_services = AsyncMock(return_value=(new_entity_info, [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, new_entity_info, []) + ) # Trigger a reconnect to simulate the entity info update await device.mock_disconnect(expected_disconnect=False) @@ -1322,6 +1344,9 @@ async def test_unique_id_migration_sub_device_to_main_device( # Update the entity info mock_client.list_entities_services = AsyncMock(return_value=(new_entity_info, [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, new_entity_info, []) + ) # Trigger a reconnect await device.mock_disconnect(expected_disconnect=False) @@ -1415,6 +1440,9 @@ async def test_unique_id_migration_between_sub_devices( # Update the entity info mock_client.list_entities_services = AsyncMock(return_value=(new_entity_info, [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, new_entity_info, []) + ) # Trigger a reconnect await device.mock_disconnect(expected_disconnect=False) @@ -1534,6 +1562,9 @@ async def test_entity_device_id_rename_in_yaml( # Update the entity info mock_client.list_entities_services = AsyncMock(return_value=(new_entity_info, [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(new_device_info, new_entity_info, []) + ) # Trigger a reconnect to simulate the YAML config change await device.mock_disconnect(expected_disconnect=False) diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index fec957a9560..86dfb6e9ea3 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -416,14 +416,17 @@ async def test_unique_id_updated_to_mac( entry.add_to_hass(hass) subscribe_done = hass.loop.create_future() - def async_subscribe_states(*args, **kwargs) -> None: + def async_subscribe_home_assistant_states_and_services(*args, **kwargs) -> None: subscribe_done.set_result(None) - mock_client.subscribe_states = async_subscribe_states - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - mac_address="1122334455aa", - ) + mock_client.subscribe_home_assistant_states_and_services = ( + async_subscribe_home_assistant_states_and_services + ) + device_info = DeviceInfo(mac_address="1122334455aa") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -447,15 +450,20 @@ async def test_add_missing_bluetooth_mac_address( entry.add_to_hass(hass) subscribe_done = hass.loop.create_future() - def async_subscribe_states(*args, **kwargs) -> None: + def async_subscribe_home_assistant_states_and_services(*args, **kwargs) -> None: subscribe_done.set_result(None) - mock_client.subscribe_states = async_subscribe_states - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - mac_address="1122334455aa", - bluetooth_mac_address="AA:BB:CC:DD:EE:FF", - ) + mock_client.subscribe_home_assistant_states_and_services = ( + async_subscribe_home_assistant_states_and_services + ) + device_info = DeviceInfo( + mac_address="1122334455aa", + bluetooth_mac_address="AA:BB:CC:DD:EE:FF", + ) + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -489,8 +497,11 @@ async def test_unique_id_not_updated_if_name_same_and_already_mac( disconnect_done.set_result(None) mock_client.disconnect = async_disconnect - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455ab", name="test") + device_info = DeviceInfo(mac_address="1122334455ab", name="test") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -519,8 +530,11 @@ async def test_unique_id_updated_if_name_unset_and_already_mac( disconnect_done.set_result(None) mock_client.disconnect = async_disconnect - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455ab", name="test") + device_info = DeviceInfo(mac_address="1122334455ab", name="test") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -554,8 +568,11 @@ async def test_unique_id_not_updated_if_name_different_and_already_mac( disconnect_done.set_result(None) mock_client.disconnect = async_disconnect - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455ab", name="different") + device_info = DeviceInfo(mac_address="1122334455ab", name="different") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -587,12 +604,17 @@ async def test_name_updated_only_if_mac_matches( entry.add_to_hass(hass) subscribe_done = hass.loop.create_future() - def async_subscribe_states(*args, **kwargs) -> None: + def async_subscribe_home_assistant_states_and_services(*args, **kwargs) -> None: subscribe_done.set_result(None) - mock_client.subscribe_states = async_subscribe_states - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455aa", name="new") + mock_client.subscribe_home_assistant_states_and_services = ( + async_subscribe_home_assistant_states_and_services + ) + device_info = DeviceInfo(mac_address="1122334455aa", name="new") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -622,12 +644,17 @@ async def test_name_updated_only_if_mac_was_unset( entry.add_to_hass(hass) subscribe_done = hass.loop.create_future() - def async_subscribe_states(*args, **kwargs) -> None: + def async_subscribe_home_assistant_states_and_services(*args, **kwargs) -> None: subscribe_done.set_result(None) - mock_client.subscribe_states = async_subscribe_states - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455aa", name="new") + mock_client.subscribe_home_assistant_states_and_services = ( + async_subscribe_home_assistant_states_and_services + ) + device_info = DeviceInfo(mac_address="1122334455aa", name="new") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -664,8 +691,11 @@ async def test_connection_aborted_wrong_device( disconnect_done.set_result(None) mock_client.disconnect = async_disconnect - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455ab", name="different") + device_info = DeviceInfo(mac_address="1122334455ab", name="different") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -695,10 +725,12 @@ async def test_connection_aborted_wrong_device( hostname="test", macaddress="1122334455aa", ) - new_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455aa", name="test") - ) + device_info = DeviceInfo(mac_address="1122334455aa", name="test") + new_info = AsyncMock(return_value=device_info) mock_client.device_info = new_info + # Also need to update device_info_and_list_entities + new_combined_info = AsyncMock(return_value=(device_info, [], [])) + mock_client.device_info_and_list_entities = new_combined_info result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) @@ -712,7 +744,8 @@ async def test_connection_aborted_wrong_device( } assert entry.data[CONF_HOST] == "192.168.43.184" await hass.async_block_till_done() - assert len(new_info.mock_calls) == 2 + # Check that either device_info or device_info_and_list_entities was called + assert len(new_info.mock_calls) + len(new_combined_info.mock_calls) == 2 assert "Unexpected device found at" not in caplog.text @@ -741,8 +774,11 @@ async def test_connection_aborted_wrong_device_same_name( disconnect_done.set_result(None) mock_client.disconnect = async_disconnect - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455ab", name="test") + device_info = DeviceInfo(mac_address="1122334455ab", name="test") + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(entry.entry_id) @@ -769,10 +805,12 @@ async def test_connection_aborted_wrong_device_same_name( hostname="test", macaddress="1122334455aa", ) - new_info = AsyncMock( - return_value=DeviceInfo(mac_address="1122334455aa", name="test") - ) + device_info = DeviceInfo(mac_address="1122334455aa", name="test") + new_info = AsyncMock(return_value=device_info) mock_client.device_info = new_info + # Also need to update device_info_and_list_entities + new_combined_info = AsyncMock(return_value=(device_info, [], [])) + mock_client.device_info_and_list_entities = new_combined_info result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) @@ -786,7 +824,8 @@ async def test_connection_aborted_wrong_device_same_name( } assert entry.data[CONF_HOST] == "192.168.43.184" await hass.async_block_till_done() - assert len(new_info.mock_calls) == 2 + # Check that either device_info or device_info_and_list_entities was called + assert len(new_info.mock_calls) + len(new_combined_info.mock_calls) == 2 assert "Unexpected device found at" not in caplog.text @@ -815,6 +854,12 @@ async def test_failure_during_connect( mock_client.disconnect = async_disconnect mock_client.device_info = AsyncMock(side_effect=APIConnectionError("fail")) + mock_client.list_entities_services = AsyncMock( + side_effect=APIConnectionError("fail") + ) + mock_client.device_info_and_list_entities = AsyncMock( + side_effect=APIConnectionError("fail") + ) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -981,6 +1026,9 @@ async def test_esphome_device_with_dash_in_name_user_services( # Verify the service can be removed mock_client.list_entities_services = AsyncMock(return_value=([], [service1])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, [], [service1]) + ) await device.mock_disconnect(True) await hass.async_block_till_done() await device.mock_connect() @@ -1037,6 +1085,9 @@ async def test_esphome_user_services_ignores_invalid_arg_types( # Verify the service can be removed mock_client.list_entities_services = AsyncMock(return_value=([], [service2])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, [], [service2]) + ) await device.mock_disconnect(True) await hass.async_block_till_done() await device.mock_connect() @@ -1145,6 +1196,9 @@ async def test_esphome_user_services_changes( # Verify the service can be updated mock_client.list_entities_services = AsyncMock(return_value=([], [new_service1])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, [], [new_service1]) + ) await device.mock_disconnect(True) await hass.async_block_till_done() await device.mock_connect() @@ -1478,6 +1532,10 @@ async def test_device_adds_friendly_name( **{**device.device_info.to_dict(), "friendly_name": "I have a friendly name"} ) mock_client.device_info = AsyncMock(return_value=device.device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, [], []) + ) await device.mock_connect() await hass.async_block_till_done() dev = dev_reg.async_get_device( @@ -1668,6 +1726,10 @@ async def test_sub_device_cleanup( # Update the mock client to return the new device info mock_client.device_info = AsyncMock(return_value=device.device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device.device_info, [], []) + ) # Simulate reconnection which triggers device registry update await device.mock_connect() diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index 232f7e1f06e..b5805298b97 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -27,8 +27,11 @@ from homeassistant.components.media_player import ( SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_PLAY_MEDIA, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + STATE_PLAYING, BrowseMedia, MediaClass, MediaType, @@ -56,6 +59,8 @@ async def test_media_player_entity( key=1, name="my media_player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY,TURN_OFF,TURN_ON + feature_flags=1201037, ) ] states = [ @@ -155,6 +160,113 @@ async def test_media_player_entity( ) mock_client.media_player_command.reset_mock() + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "media_player.test_my_media_player", + }, + blocking=True, + ) + mock_client.media_player_command.assert_has_calls( + [call(1, command=MediaPlayerCommand.TURN_OFF, device_id=0)] + ) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "media_player.test_my_media_player", + }, + blocking=True, + ) + mock_client.media_player_command.assert_has_calls( + [call(1, command=MediaPlayerCommand.TURN_ON, device_id=0)] + ) + mock_client.media_player_command.reset_mock() + + +async def test_media_player_entity_with_undefined_flags( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry: MockGenericDeviceEntryType, +) -> None: + """Test that media_player handles undefined feature flags gracefully.""" + # Include existing flags (PAUSE=1, PLAY=16384, VOLUME_SET=4) + # plus undefined bits (bit 6=64, bit 23=8388608) + # Total: 1 + 16384 + 4 + 64 + 8388608 = 8405061 + entity_info = [ + MediaPlayerInfo( + object_id="mymedia_player_undefined", + key=1, + name="my media_player undefined", + supports_pause=True, + # PAUSE,PLAY,VOLUME_SET + undefined bits 6 and 23 + feature_flags=8405061, + ) + ] + states = [ + MediaPlayerEntityState( + key=1, volume=50, muted=False, state=MediaPlayerState.PLAYING + ) + ] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + states=states, + ) + + # Verify entity is created successfully despite undefined flags + state = hass.states.get("media_player.test_my_media_player_undefined") + assert state is not None + assert state.state == STATE_PLAYING + + # Verify supported features only include known flags + # Should have PAUSE, PLAY, and VOLUME_SET + supported_features = state.attributes.get("supported_features", 0) + # PAUSE=1, VOLUME_SET=4, PLAY=16384 = 16389 + assert supported_features == 16389 + + # Verify entity works correctly with known features + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PLAY, + { + ATTR_ENTITY_ID: "media_player.test_my_media_player_undefined", + }, + blocking=True, + ) + mock_client.media_player_command.assert_has_calls( + [call(1, command=MediaPlayerCommand.PLAY, device_id=0)] + ) + mock_client.media_player_command.reset_mock() + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PAUSE, + { + ATTR_ENTITY_ID: "media_player.test_my_media_player_undefined", + }, + blocking=True, + ) + mock_client.media_player_command.assert_has_calls( + [call(1, command=MediaPlayerCommand.PAUSE, device_id=0)] + ) + mock_client.media_player_command.reset_mock() + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_VOLUME_SET, + { + ATTR_ENTITY_ID: "media_player.test_my_media_player_undefined", + ATTR_MEDIA_VOLUME_LEVEL: 0.7, + }, + blocking=True, + ) + mock_client.media_player_command.assert_has_calls( + [call(1, volume=0.7, device_id=0)] + ) + async def test_media_player_entity_with_source( hass: HomeAssistant, @@ -202,6 +314,8 @@ async def test_media_player_entity_with_source( key=1, name="my media_player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY + feature_flags=1200653, ) ] states = [ @@ -317,6 +431,8 @@ async def test_media_player_proxy( key=1, name="my media_player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY + feature_flags=1200653, supported_formats=[ MediaPlayerSupportedFormat( format="flac", @@ -475,6 +591,8 @@ async def test_media_player_formats_reload_preserves_data( key=1, name="Test Media Player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY + feature_flags=1200653, supported_formats=supported_formats, ) ], diff --git a/tests/components/esphome/test_repairs.py b/tests/components/esphome/test_repairs.py index f5142367432..f64cb806950 100644 --- a/tests/components/esphome/test_repairs.py +++ b/tests/components/esphome/test_repairs.py @@ -55,10 +55,13 @@ async def test_device_conflict_manual( disconnect_done.set_result(None) mock_client.disconnect = async_disconnect - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - mac_address="1122334455ab", name="test", model="esp32-iso-poe" - ) + device_info = DeviceInfo( + mac_address="1122334455ab", name="test", model="esp32-iso-poe" + ) + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -102,10 +105,13 @@ async def test_device_conflict_manual( assert data["type"] == FlowResultType.FORM assert data["step_id"] == "manual" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - mac_address="11:22:33:44:55:aa", name="test", model="esp32-iso-poe" - ) + device_info = DeviceInfo( + mac_address="11:22:33:44:55:aa", name="test", model="esp32-iso-poe" + ) + mock_client.device_info = AsyncMock(return_value=device_info) + mock_client.list_entities_services = AsyncMock(return_value=([], [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(device_info, [], []) ) caplog.clear() data = await process_repair_fix_flow(client, flow_id) @@ -169,6 +175,11 @@ async def test_device_conflict_migration( mac_address="11:22:33:44:55:AB", name="test", model="esp32-iso-poe" ) mock_client.device_info = AsyncMock(return_value=new_device_info) + # Keep the same entity_info when reloading + mock_client.list_entities_services = AsyncMock(return_value=(entity_info, [])) + mock_client.device_info_and_list_entities = AsyncMock( + return_value=(new_device_info, entity_info, []) + ) device.device_info = new_device_info await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index bef44c92f34..800412fc9d4 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -125,7 +125,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/fan/test_intent.py b/tests/components/fan/test_intent.py new file mode 100644 index 00000000000..450d81e9dff --- /dev/null +++ b/tests/components/fan/test_intent.py @@ -0,0 +1,37 @@ +"""Intent tests for the fan platform.""" + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + DOMAIN, + SERVICE_TURN_ON, + intent as fan_intent, +) +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from tests.common import async_mock_service + + +async def test_set_speed_intent(hass: HomeAssistant) -> None: + """Test set speed intent for fans.""" + await fan_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_fan" + hass.states.async_set(entity_id, STATE_OFF) + calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + + response = await intent.async_handle( + hass, + "test", + fan_intent.INTENT_FAN_SET_SPEED, + {"name": {"value": "test fan"}, ATTR_PERCENTAGE: {"value": 50}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data == {"entity_id": entity_id, "percentage": 50} diff --git a/tests/components/foscam/conftest.py b/tests/components/foscam/conftest.py index f8b4093574f..43616693303 100644 --- a/tests/components/foscam/conftest.py +++ b/tests/components/foscam/conftest.py @@ -60,6 +60,21 @@ def setup_mock_foscam_camera(mock_foscam_camera): mock_foscam_camera.get_dev_info.return_value = (dev_info_rc, dev_info_data) mock_foscam_camera.get_port_info.return_value = (dev_info_rc, {}) mock_foscam_camera.is_asleep.return_value = (0, True) + mock_foscam_camera.get_infra_led_config.return_value = (0, {"mode": "1"}) + mock_foscam_camera.get_mirror_and_flip_setting.return_value = ( + 0, + {"isFlip": "0", "isMirror": "0"}, + ) + mock_foscam_camera.is_asleep.return_value = (0, "0") + mock_foscam_camera.getWhiteLightBrightness.return_value = (0, {"enable": "1"}) + mock_foscam_camera.getSirenConfig.return_value = (0, {"sirenEnable": "1"}) + mock_foscam_camera.getAudioVolume.return_value = (0, {"volume": "100"}) + mock_foscam_camera.getSpeakVolume.return_value = (0, {"SpeakVolume": "100"}) + mock_foscam_camera.getVoiceEnableState.return_value = (0, {"isEnable": "1"}) + mock_foscam_camera.getLedEnableState.return_value = (0, {"isEnable": "0"}) + mock_foscam_camera.getWdrMode.return_value = (0, {"mode": "0"}) + mock_foscam_camera.getHdrMode.return_value = (0, {"mode": "0"}) + mock_foscam_camera.get_motion_detect_config.return_value = (0, 1) return mock_foscam_camera diff --git a/tests/components/foscam/snapshots/test_switch.ambr b/tests/components/foscam/snapshots/test_switch.ambr new file mode 100644 index 00000000000..f48df6b65e6 --- /dev/null +++ b/tests/components/foscam/snapshots/test_switch.ambr @@ -0,0 +1,385 @@ +# serializer version: 1 +# name: test_entities[switch.mock_title_flip-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip_switch', + 'unique_id': '123ABC_is_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Flip', + }), + 'context': , + 'entity_id': 'switch.mock_title_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_infrared_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_infrared_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Infrared mode', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ir_switch', + 'unique_id': '123ABC_is_open_ir', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_infrared_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Infrared mode', + }), + 'context': , + 'entity_id': 'switch.mock_title_infrared_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[switch.mock_title_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_off_light_switch', + 'unique_id': '123ABC_is_turn_off_light', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Light', + }), + 'context': , + 'entity_id': 'switch.mock_title_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_mirror-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_mirror', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mirror', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mirror_switch', + 'unique_id': '123ABC_is_mirror', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_mirror-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Mirror', + }), + 'context': , + 'entity_id': 'switch.mock_title_mirror', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_siren_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_siren_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Siren alarm', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'siren_alarm_switch', + 'unique_id': '123ABC_is_siren_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_siren_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Siren alarm', + }), + 'context': , + 'entity_id': 'switch.mock_title_siren_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[switch.mock_title_sleep_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_sleep_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sleep mode', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sleep_switch', + 'unique_id': '123ABC_sleep_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_sleep_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Sleep mode', + }), + 'context': , + 'entity_id': 'switch.mock_title_sleep_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_volume_muted-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_volume_muted', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume muted', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_off_volume_switch', + 'unique_id': '123ABC_is_turn_off_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_volume_muted-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Volume muted', + }), + 'context': , + 'entity_id': 'switch.mock_title_volume_muted', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.mock_title_white_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_title_white_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'White light', + 'platform': 'foscam', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'white_light_switch', + 'unique_id': '123ABC_is_open_white_light', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.mock_title_white_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title White light', + }), + 'context': , + 'entity_id': 'switch.mock_title_white_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/foscam/test_init.py b/tests/components/foscam/test_init.py index a7b6a8c8f0b..7c7b1b8aee8 100644 --- a/tests/components/foscam/test_init.py +++ b/tests/components/foscam/test_init.py @@ -96,7 +96,7 @@ async def test_unique_id_migration_not_needed( assert entity_before.unique_id == f"{ENTRY_ID}_sleep_switch" with ( - # Mock a valid camera instance" + # Mock a valid camera instance patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera, patch( "homeassistant.components.foscam.async_migrate_entry", diff --git a/tests/components/foscam/test_switch.py b/tests/components/foscam/test_switch.py new file mode 100644 index 00000000000..bd9eb380fbd --- /dev/null +++ b/tests/components/foscam/test_switch.py @@ -0,0 +1,35 @@ +"""Test for the switch platform entity of the foscam component.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.foscam.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_mock_foscam_camera +from .const import ENTRY_ID, VALID_CONFIG + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test that coordinator returns the data we expect after the first refresh.""" + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID) + entry.add_to_hass(hass) + + with ( + # Mock a valid camera instance" + patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera, + patch("homeassistant.components.foscam.PLATFORMS", [Platform.SWITCH]), + ): + setup_mock_foscam_camera(mock_foscam_camera) + assert await hass.config_entries.async_setup(entry.entry_id) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) diff --git a/tests/components/fyta/snapshots/test_sensor.ambr b/tests/components/fyta/snapshots/test_sensor.ambr index 5227755d852..289927a587b 100644 --- a/tests/components/fyta/snapshots/test_sensor.ambr +++ b/tests/components/fyta/snapshots/test_sensor.ambr @@ -142,6 +142,10 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Gummibaum Light', + 'max_acceptable': 675.0, + 'max_good': 450.0, + 'min_acceptable': 18.0, + 'min_good': 20.0, 'state_class': , 'unit_of_measurement': 'μmol/s⋅m²', }), @@ -261,6 +265,10 @@ 'attributes': ReadOnlyDict({ 'device_class': 'moisture', 'friendly_name': 'Gummibaum Moisture', + 'max_acceptable': 80.0, + 'max_good': 70.0, + 'min_acceptable': 25.0, + 'min_good': 35.0, 'state_class': , 'unit_of_measurement': '%', }), @@ -612,6 +620,10 @@ 'attributes': ReadOnlyDict({ 'device_class': 'conductivity', 'friendly_name': 'Gummibaum Salinity', + 'max_acceptable': 1.2, + 'max_good': 1.0, + 'min_acceptable': 0.4, + 'min_good': 0.6, 'state_class': , 'unit_of_measurement': , }), @@ -782,6 +794,10 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Gummibaum Temperature', + 'max_acceptable': 42.0, + 'max_good': 36.0, + 'min_acceptable': 10.0, + 'min_good': 17.0, 'state_class': , 'unit_of_measurement': , }), @@ -1002,6 +1018,10 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Kakaobaum Light', + 'max_acceptable': 675.0, + 'max_good': 450.0, + 'min_acceptable': 18.0, + 'min_good': 20.0, 'state_class': , 'unit_of_measurement': 'μmol/s⋅m²', }), @@ -1121,6 +1141,10 @@ 'attributes': ReadOnlyDict({ 'device_class': 'moisture', 'friendly_name': 'Kakaobaum Moisture', + 'max_acceptable': 80.0, + 'max_good': 70.0, + 'min_acceptable': 25.0, + 'min_good': 35.0, 'state_class': , 'unit_of_measurement': '%', }), @@ -1472,6 +1496,10 @@ 'attributes': ReadOnlyDict({ 'device_class': 'conductivity', 'friendly_name': 'Kakaobaum Salinity', + 'max_acceptable': 1.2, + 'max_good': 1.0, + 'min_acceptable': 0.4, + 'min_good': 0.6, 'state_class': , 'unit_of_measurement': , }), @@ -1642,6 +1670,10 @@ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', 'friendly_name': 'Kakaobaum Temperature', + 'max_acceptable': 42.0, + 'max_good': 36.0, + 'min_acceptable': 10.0, + 'min_good': 17.0, 'state_class': , 'unit_of_measurement': , }), diff --git a/tests/components/gios/snapshots/test_sensor.ambr b/tests/components/gios/snapshots/test_sensor.ambr index 2a0afcc72b1..b7ad5b2d51d 100644 --- a/tests/components/gios/snapshots/test_sensor.ambr +++ b/tests/components/gios/snapshots/test_sensor.ambr @@ -103,7 +103,7 @@ 'supported_features': 0, 'translation_key': 'c6h6', 'unique_id': '123-c6h6', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_benzene-state] @@ -112,7 +112,7 @@ 'attribution': 'Data provided by GIOŚ', 'friendly_name': 'Home Benzene', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_benzene', @@ -159,7 +159,7 @@ 'supported_features': 0, 'translation_key': 'co', 'unique_id': '123-co', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_carbon_monoxide-state] @@ -168,7 +168,7 @@ 'attribution': 'Data provided by GIOŚ', 'friendly_name': 'Home Carbon monoxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_carbon_monoxide', @@ -215,7 +215,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-no2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_nitrogen_dioxide-state] @@ -225,7 +225,7 @@ 'device_class': 'nitrogen_dioxide', 'friendly_name': 'Home Nitrogen dioxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_nitrogen_dioxide', @@ -339,7 +339,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-no', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_nitrogen_monoxide-state] @@ -349,7 +349,7 @@ 'device_class': 'nitrogen_monoxide', 'friendly_name': 'Home Nitrogen monoxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_nitrogen_monoxide', @@ -396,7 +396,7 @@ 'supported_features': 0, 'translation_key': 'nox', 'unique_id': '123-nox', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_nitrogen_oxides-state] @@ -405,7 +405,7 @@ 'attribution': 'Data provided by GIOŚ', 'friendly_name': 'Home Nitrogen oxides', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_nitrogen_oxides', @@ -452,7 +452,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-o3', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_ozone-state] @@ -462,7 +462,7 @@ 'device_class': 'ozone', 'friendly_name': 'Home Ozone', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_ozone', @@ -576,7 +576,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-pm10', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_pm10-state] @@ -586,7 +586,7 @@ 'device_class': 'pm10', 'friendly_name': 'Home PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_pm10', @@ -700,7 +700,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-pm25', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_pm2_5-state] @@ -710,7 +710,7 @@ 'device_class': 'pm25', 'friendly_name': 'Home PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_pm2_5', @@ -824,7 +824,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '123-so2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.home_sulphur_dioxide-state] @@ -834,7 +834,7 @@ 'device_class': 'sulphur_dioxide', 'friendly_name': 'Home Sulphur dioxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_sulphur_dioxide', diff --git a/tests/components/google_generative_ai_conversation/__init__.py b/tests/components/google_generative_ai_conversation/__init__.py index 18b3c8e07f0..57119ce0ff1 100644 --- a/tests/components/google_generative_ai_conversation/__init__.py +++ b/tests/components/google_generative_ai_conversation/__init__.py @@ -1,43 +1,16 @@ """Tests for the Google Generative AI Conversation integration.""" -from unittest.mock import Mock - from google.genai.errors import APIError, ClientError -import httpx API_ERROR_500 = APIError( 500, - Mock( - __class__=httpx.Response, - json=Mock( - return_value={ - "message": "Internal Server Error", - "status": "internal-error", - } - ), - ), + {"message": "Internal Server Error", "status": "internal-error"}, ) CLIENT_ERROR_BAD_REQUEST = ClientError( 400, - Mock( - __class__=httpx.Response, - json=Mock( - return_value={ - "message": "Bad Request", - "status": "invalid-argument", - } - ), - ), + {"message": "Bad Request", "status": "invalid-argument"}, ) CLIENT_ERROR_API_KEY_INVALID = ClientError( 400, - Mock( - __class__=httpx.Response, - json=Mock( - return_value={ - "message": "'reason': API_KEY_INVALID", - "status": "unauthorized", - } - ), - ), + {"message": "'reason': API_KEY_INVALID", "status": "unauthorized"}, ) diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index d0b92a7e88d..c2568159c79 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -128,8 +128,14 @@ dict({ 'contents': list([ 'Describe this image from my doorbell camera', - File(name='doorbell_snapshot.jpg', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), - File(name='context.txt', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), + File( + name='doorbell_snapshot.jpg', + state= + ), + File( + name='context.txt', + state= + ), ]), 'model': 'models/gemini-2.5-flash', }), @@ -145,8 +151,14 @@ dict({ 'contents': list([ 'Describe this image from my doorbell camera', - File(name='doorbell_snapshot.jpg', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), - File(name='context.txt', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), + File( + name='doorbell_snapshot.jpg', + state= + ), + File( + name='context.txt', + state= + ), ]), 'model': 'models/gemini-2.5-flash', }), diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 90f496b4b5b..ab8c10e933b 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -195,10 +195,13 @@ async def test_function_call( "response": { "result": "Test response", }, + "scheduling": None, + "will_continue": None, }, "inline_data": None, "text": None, "thought": None, + "thought_signature": None, "video_metadata": None, } diff --git a/tests/components/google_generative_ai_conversation/test_tts.py b/tests/components/google_generative_ai_conversation/test_tts.py index 108ac82947c..87fc4fe8a76 100644 --- a/tests/components/google_generative_ai_conversation/test_tts.py +++ b/tests/components/google_generative_ai_conversation/test_tts.py @@ -37,7 +37,7 @@ from tests.common import MockConfigEntry, async_mock_service from tests.components.tts.common import retrieve_media from tests.typing import ClientSessionGenerator -API_ERROR_500 = APIError("test", response=MagicMock()) +API_ERROR_500 = APIError("test", response_json={}) TEST_CHAT_MODEL = "models/some-tts-model" diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index caed4a5c469..2410b5dbbde 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -183,7 +183,7 @@ async def test_gvh5106(hass: HomeAssistant) -> None: pm25_sensor_attributes = pm25_sensor.attributes assert pm25_sensor.state == "0" assert pm25_sensor_attributes[ATTR_FRIENDLY_NAME] == "H5106 4E05 Pm25" - assert pm25_sensor_attributes[ATTR_UNIT_OF_MEASUREMENT] == "µg/m³" + assert pm25_sensor_attributes[ATTR_UNIT_OF_MEASUREMENT] == "μg/m³" assert pm25_sensor_attributes[ATTR_STATE_CLASS] == "measurement" assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 322e6ebdad0..b1bb6e5d7bb 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -44,7 +44,8 @@ from tests.typing import WebSocketGenerator {}, ), ("fan", "on", "on", {}, {}, {}, {}), - ("light", "on", "on", {}, {}, {}, {}), + ("light", "on", "on", {}, {}, {"all": False}, {}), + ("light", "on", "on", {}, {"all": True}, {"all": True}, {}), ("lock", "locked", "locked", {}, {}, {}, {}), ("notify", STATE_UNKNOWN, "2021-01-01T23:59:59.123+00:00", {}, {}, {}, {}), ("media_player", "on", "on", {}, {}, {}, {}), @@ -57,7 +58,8 @@ from tests.typing import WebSocketGenerator {"type": "sum"}, {}, ), - ("switch", "on", "on", {}, {}, {}, {}), + ("switch", "on", "on", {}, {}, {"all": False}, {}), + ("switch", "on", "on", {}, {"all": True}, {"all": True}, {}), ], ) async def test_config_flow( diff --git a/tests/components/habitica/conftest.py b/tests/components/habitica/conftest.py index 80e09d823cc..331d2ccf36a 100644 --- a/tests/components/habitica/conftest.py +++ b/tests/components/habitica/conftest.py @@ -10,6 +10,7 @@ from habiticalib import ( HabiticaContentResponse, HabiticaErrorResponse, HabiticaGroupMembersResponse, + HabiticaGroupsResponse, HabiticaLoginResponse, HabiticaQuestResponse, HabiticaResponse, @@ -155,6 +156,9 @@ async def mock_habiticalib(hass: HomeAssistant) -> AsyncGenerator[AsyncMock]: client.create_task.return_value = HabiticaTaskResponse.from_json( await async_load_fixture(hass, "task.json", DOMAIN) ) + client.get_group.return_value = HabiticaGroupsResponse.from_json( + await async_load_fixture(hass, "party.json", DOMAIN) + ) yield client diff --git a/tests/components/habitica/fixtures/party.json b/tests/components/habitica/fixtures/party.json new file mode 100644 index 00000000000..18e7936ca85 --- /dev/null +++ b/tests/components/habitica/fixtures/party.json @@ -0,0 +1,75 @@ +{ + "success": true, + "data": { + "leaderOnly": { + "challenges": false, + "getGems": false + }, + "quest": { + "progress": { + "collect": { + "soapBars": 10 + } + }, + "key": "atom1", + "active": true, + "leader": "d69833ef-4542-4259-ba50-9b4a1a841bcf", + "members": { + "d69833ef-4542-4259-ba50-9b4a1a841bcf": true + }, + "extra": {} + }, + "tasksOrder": { + "habits": [], + "dailys": [], + "todos": [], + "rewards": [] + }, + "purchased": { + "plan": { + "consecutive": { + "count": 0, + "offset": 0, + "gemCapExtra": 0, + "trinkets": 0 + }, + "quantity": 1, + "extraMonths": 0, + "gemsBought": 0, + "cumulativeCount": 0, + "mysteryItems": [] + } + }, + "cron": {}, + "_id": "1e87097c-4c03-4f8c-a475-67cc7da7f409", + "name": "test-user's Party", + "type": "party", + "privacy": "private", + "chat": [], + "memberCount": 2, + "challengeCount": 0, + "balance": 0, + "managers": {}, + "categories": [], + "leader": { + "auth": { + "local": { + "username": "test-username" + } + }, + "flags": { + "verifiedUsername": true + }, + "profile": { + "name": "test-user" + }, + "_id": "af36e2a8-7927-4dec-a258-400ade7f0ae3", + "id": "af36e2a8-7927-4dec-a258-400ade7f0ae3" + }, + "summary": "test-user's Party", + "id": "1e87097c-4c03-4f8c-a475-67cc7da7f409" + }, + "notifications": [], + "userV": 0, + "appVersion": "5.38.0" +} diff --git a/tests/components/habitica/fixtures/party_2.json b/tests/components/habitica/fixtures/party_2.json new file mode 100644 index 00000000000..2c0ff528e32 --- /dev/null +++ b/tests/components/habitica/fixtures/party_2.json @@ -0,0 +1,74 @@ +{ + "success": true, + "data": { + "leaderOnly": { + "challenges": false, + "getGems": false + }, + "quest": { + "progress": { + "collect": {}, + "hp": 100 + }, + "key": "dustbunnies", + "active": true, + "leader": "d69833ef-4542-4259-ba50-9b4a1a841bcf", + "members": { + "d69833ef-4542-4259-ba50-9b4a1a841bcf": true + }, + "extra": {} + }, + "tasksOrder": { + "habits": [], + "dailys": [], + "todos": [], + "rewards": [] + }, + "purchased": { + "plan": { + "consecutive": { + "count": 0, + "offset": 0, + "gemCapExtra": 0, + "trinkets": 0 + }, + "quantity": 1, + "extraMonths": 0, + "gemsBought": 0, + "cumulativeCount": 0, + "mysteryItems": [] + } + }, + "cron": {}, + "_id": "1e87097c-4c03-4f8c-a475-67cc7da7f409", + "name": "test-user's Party", + "type": "party", + "privacy": "private", + "chat": [], + "memberCount": 2, + "challengeCount": 0, + "balance": 0, + "managers": {}, + "categories": [], + "leader": { + "auth": { + "local": { + "username": "test-username" + } + }, + "flags": { + "verifiedUsername": true + }, + "profile": { + "name": "test-user" + }, + "_id": "af36e2a8-7927-4dec-a258-400ade7f0ae3", + "id": "af36e2a8-7927-4dec-a258-400ade7f0ae3" + }, + "summary": "test-user's Party", + "id": "1e87097c-4c03-4f8c-a475-67cc7da7f409" + }, + "notifications": [], + "userV": 0, + "appVersion": "5.38.0" +} diff --git a/tests/components/habitica/fixtures/user.json b/tests/components/habitica/fixtures/user.json index d2f0091b6dd..28faec64dc9 100644 --- a/tests/components/habitica/fixtures/user.json +++ b/tests/components/habitica/fixtures/user.json @@ -76,7 +76,7 @@ "RSVPNeeded": true, "key": "dustbunnies" }, - "_id": "94cd398c-2240-4320-956e-6d345cf2c0de" + "_id": "1e87097c-4c03-4f8c-a475-67cc7da7f409" }, "tags": [ { diff --git a/tests/components/habitica/fixtures/user_no_party.json b/tests/components/habitica/fixtures/user_no_party.json index 1c58dde6f50..bd447b1af67 100644 --- a/tests/components/habitica/fixtures/user_no_party.json +++ b/tests/components/habitica/fixtures/user_no_party.json @@ -55,7 +55,9 @@ "e97659e0-2c42-4599-a7bb-00282adc410d", "564b9ac9-c53d-4638-9e7f-1cd96fe19baa", "f2c85972-1a19-4426-bc6d-ce3337b9d99f", - "2c6d136c-a1c3-4bef-b7c4-fa980784b1e1" + "2c6d136c-a1c3-4bef-b7c4-fa980784b1e1", + "6e53f1f5-a315-4edd-984d-8d762e4a08ef", + "369afeed-61e3-4bf7-9747-66e05807134c" ], "habits": ["1d147de6-5c02-4740-8e2f-71d3015a37f4"] }, diff --git a/tests/components/habitica/snapshots/test_binary_sensor.ambr b/tests/components/habitica/snapshots/test_binary_sensor.ambr index 247063f2ae8..64dbc160a1b 100644 --- a/tests/components/habitica/snapshots/test_binary_sensor.ambr +++ b/tests/components/habitica/snapshots/test_binary_sensor.ambr @@ -48,3 +48,52 @@ 'state': 'on', }) # --- +# name: test_binary_sensors[binary_sensor.test_user_s_party_quest_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_user_s_party_quest_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Quest status', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_quest_running', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.test_user_s_party_quest_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': "test-user's Party Quest status", + }), + 'context': , + 'entity_id': 'binary_sensor.test_user_s_party_quest_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/habitica/snapshots/test_sensor.ambr b/tests/components/habitica/snapshots/test_sensor.ambr index 30c0f9d66eb..89d6936f111 100644 --- a/tests/components/habitica/snapshots/test_sensor.ambr +++ b/tests/components/habitica/snapshots/test_sensor.ambr @@ -1064,6 +1064,360 @@ 'state': '2', }) # --- +# name: test_sensors[sensor.test_user_s_party_boss_health-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_s_party_boss_health', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Boss health', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_boss_hp', + 'unit_of_measurement': 'HP', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_boss_health-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSItNiAtNiAzNiAzNiI+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxwYXRoIGZpbGw9IiNGNzRFNTIiIGQ9Ik0yIDQuNUw2LjE2NyAyIDEyIDUuMTY3IDE3LjgzMyAyIDIyIDQuNVYxMmwtNC4xNjcgNS44MzNMMTIgMjJsLTUuODMzLTQuMTY3TDIgMTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGNjE2NSIgZD0iTTcuMzMzIDE2LjY2N0wzLjY2NyAxMS41VjUuNDE3bDIuNS0xLjVMMTIgNy4wODNsNS44MzMtMy4xNjYgMi41IDEuNVYxMS41bC0zLjY2NiA1LjE2N0wxMiAxOS45MTd6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTEyIDE0LjA4M2w0LjY2NyAyLjU4NEwxMiAxOS45MTd6IiBvcGFjaXR5PSIuNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNCNTI0MjgiIGQ9Ik0xMiAxNC4wODNsLTQuNjY3IDIuNTg0TDEyIDE5LjkxN3oiIG9wYWNpdHk9Ii4zNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03LjMzMyAxNi42NjdMMy42NjcgMTEuNSAxMiAxNC4wODN6IiBvcGFjaXR5PSIuMjUiLz4KICAgICAgICA8cGF0aCBmaWxsPSIjQjUyNDI4IiBkPSJNMTYuNjY3IDE2LjY2N2wzLjY2Ni01LjE2N0wxMiAxNC4wODN6IiBvcGFjaXR5PSIuNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNCNTI0MjgiIGQ9Ik0xMiAxNC4wODNsNS44MzMtMTAuMTY2IDIuNSAxLjVWMTEuNXoiIG9wYWNpdHk9Ii4zNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNCNTI0MjgiIGQ9Ik0xMiAxNC4wODNMNi4xNjcgMy45MTdsLTIuNSAxLjVWMTEuNXoiIG9wYWNpdHk9Ii41Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTEyIDE0LjA4M0w2LjE2NyAzLjkxNyAxMiA3LjA4M3oiIG9wYWNpdHk9Ii41Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTEyIDE0LjA4M2w1LjgzMy0xMC4xNjZMMTIgNy4wODN6IiBvcGFjaXR5PSIuMjUiLz4KICAgICAgICA8cGF0aCBmaWxsPSIjRkZGIiBkPSJNOS4xNjcgMTQuODMzbC0zLTQuMTY2VjYuODMzaC4wODNMMTIgOS45MTdsNS43NS0zLjA4NGguMDgzdjMuODM0bC0zIDQuMTY2TDEyIDE2LjkxN3oiIG9wYWNpdHk9Ii41Ii8+CiAgICA8L2c+Cjwvc3ZnPg==', + 'friendly_name': "test-user's Party Boss health", + 'unit_of_measurement': 'HP', + }), + 'context': , + 'entity_id': 'sensor.test_user_s_party_boss_health', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_boss_health_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_s_party_boss_health_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Boss health remaining', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_boss_hp_remaining', + 'unit_of_measurement': 'HP', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_boss_health_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSItNiAtNiAzNiAzNiI+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxwYXRoIGZpbGw9IiNGNzRFNTIiIGQ9Ik0yIDQuNUw2LjE2NyAyIDEyIDUuMTY3IDE3LjgzMyAyIDIyIDQuNVYxMmwtNC4xNjcgNS44MzNMMTIgMjJsLTUuODMzLTQuMTY3TDIgMTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGNjE2NSIgZD0iTTcuMzMzIDE2LjY2N0wzLjY2NyAxMS41VjUuNDE3bDIuNS0xLjVMMTIgNy4wODNsNS44MzMtMy4xNjYgMi41IDEuNVYxMS41bC0zLjY2NiA1LjE2N0wxMiAxOS45MTd6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTEyIDE0LjA4M2w0LjY2NyAyLjU4NEwxMiAxOS45MTd6IiBvcGFjaXR5PSIuNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNCNTI0MjgiIGQ9Ik0xMiAxNC4wODNsLTQuNjY3IDIuNTg0TDEyIDE5LjkxN3oiIG9wYWNpdHk9Ii4zNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik03LjMzMyAxNi42NjdMMy42NjcgMTEuNSAxMiAxNC4wODN6IiBvcGFjaXR5PSIuMjUiLz4KICAgICAgICA8cGF0aCBmaWxsPSIjQjUyNDI4IiBkPSJNMTYuNjY3IDE2LjY2N2wzLjY2Ni01LjE2N0wxMiAxNC4wODN6IiBvcGFjaXR5PSIuNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNCNTI0MjgiIGQ9Ik0xMiAxNC4wODNsNS44MzMtMTAuMTY2IDIuNSAxLjVWMTEuNXoiIG9wYWNpdHk9Ii4zNSIvPgogICAgICAgIDxwYXRoIGZpbGw9IiNCNTI0MjgiIGQ9Ik0xMiAxNC4wODNMNi4xNjcgMy45MTdsLTIuNSAxLjVWMTEuNXoiIG9wYWNpdHk9Ii41Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTEyIDE0LjA4M0w2LjE2NyAzLjkxNyAxMiA3LjA4M3oiIG9wYWNpdHk9Ii41Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTEyIDE0LjA4M2w1LjgzMy0xMC4xNjZMMTIgNy4wODN6IiBvcGFjaXR5PSIuMjUiLz4KICAgICAgICA8cGF0aCBmaWxsPSIjRkZGIiBkPSJNOS4xNjcgMTQuODMzbC0zLTQuMTY2VjYuODMzaC4wODNMMTIgOS45MTdsNS43NS0zLjA4NGguMDgzdjMuODM0bC0zIDQuMTY2TDEyIDE2LjkxN3oiIG9wYWNpdHk9Ii41Ii8+CiAgICA8L2c+Cjwvc3ZnPg==', + 'friendly_name': "test-user's Party Boss health remaining", + 'unit_of_measurement': 'HP', + }), + 'context': , + 'entity_id': 'sensor.test_user_s_party_boss_health_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_collected_quest_items-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_s_party_collected_quest_items', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Collected quest items', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_collected_items', + 'unit_of_measurement': 'items', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_collected_quest_items-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'Seifenstücke': '10 / 20', + 'entity_picture': 'https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_atom1_soapBars.png', + 'friendly_name': "test-user's Party Collected quest items", + 'unit_of_measurement': 'items', + }), + 'context': , + 'entity_id': 'sensor.test_user_s_party_collected_quest_items', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_group_leader-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_s_party_group_leader', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Group leader', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_group_leader', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_user_s_party_group_leader-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': "test-user's Party Group leader", + }), + 'context': , + 'entity_id': 'sensor.test_user_s_party_group_leader', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'test-user', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_member_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_s_party_member_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Member count', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_member_count', + 'unit_of_measurement': 'members', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_member_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii02IC02IDQ2IDUwIj48dGl0bGU+QnJvbnplX1NtYWxsPC90aXRsZT48cGF0aCBkPSJNMjAsMzYuMjhDNy4xOCwzMC42MSw1LjYsMjQuNjksNS41LDEwLjQyTDIwLDMuNzVsMTQuNSw2LjY3QzM0LjQsMjQuNjksMzIuODIsMzAuNjEsMjAsMzYuMjhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMyAtMSkiIGZpbGw9IiNlYThjMzEiPjwvcGF0aD48cGF0aCBkPSJNMjAsNi41TDMyLDEyYy0wLjE1LDExLjU2LTEuNTEsMTYuNjItMTIsMjEuNTFDOS41MywyOC42NCw4LjE3LDIzLjU4LDgsMTJMMjAsNi41TTIwLDFMMyw4LjgyQzMsMjQuNDcsNC4xMywzMi4yOSwyMCwzOSwzNS44NywzMi4yOSwzNywyNC40NywzNyw4LjgyTDIwLDFoMFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zIC0xKSIgZmlsbD0iI2IzNjIxMyI+PC9wYXRoPjxwYXRoIGQ9Ik0yMCw0LjNsMTQsNi40NGMtMC4xMiwxMy43Mi0xLjcyLDE5LjUxLTE0LDI1QzcuNzMsMzAuMjUsNi4xMiwyNC40Niw2LDEwLjc0TDIwLDQuM00yMCwxTDMsOC44MkMzLDI0LjQ3LDQuMTMsMzIuMjksMjAsMzksMzUuODcsMzIuMjksMzcsMjQuNDcsMzcsOC44MkwyMCwxaDBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMyAtMSkiIGZpbGw9IiNkNzdhMjAiPjwvcGF0aD48L3N2Zz4=', + 'friendly_name': "test-user's Party Member count", + 'unit_of_measurement': 'members', + }), + 'context': , + 'entity_id': 'sensor.test_user_s_party_member_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_quest-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_s_party_quest', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Quest', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_quest', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_user_s_party_quest-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'https://habitica-assets.s3.amazonaws.com/mobileApp/images/inventory_quest_scroll_atom1.png', + 'friendly_name': "test-user's Party Quest", + 'quest_details': 'Du erreichst die Ufer des Waschbeckensees für eine wohlverdiente Auszeit ... Aber der See ist verschmutzt mit nicht abgespültem Geschirr! Wie ist das passiert? Wie auch immer, Du kannst den See jedenfalls nicht in diesem Zustand lassen. Es gibt nur eine Sache die Du tun kannst: Abspülen und den Ferienort retten! Dazu musst Du aber Seife für den Abwasch finden. Viel Seife ...', + 'quest_participants': '1 / 2', + }), + 'context': , + 'entity_id': 'sensor.test_user_s_party_quest', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Angriff des Banalen, Teil 1: Abwasch-Katastrophe!', + }) +# --- +# name: test_sensors[sensor.test_user_s_party_quest_boss-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_s_party_quest_boss', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Quest boss', + 'platform': 'habitica', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_1e87097c-4c03-4f8c-a475-67cc7da7f409_boss', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_user_s_party_quest_boss-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': "test-user's Party Quest boss", + }), + 'context': , + 'entity_id': 'sensor.test_user_s_party_quest_boss', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.test_user_saddles-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/habitica/test_image.py b/tests/components/habitica/test_image.py index 42a87d21a8a..b0810d8e76f 100644 --- a/tests/components/habitica/test_image.py +++ b/tests/components/habitica/test_image.py @@ -8,12 +8,13 @@ import sys from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from habiticalib import HabiticaUserResponse +from habiticalib import HabiticaGroupsResponse, HabiticaUserResponse import pytest +import respx from syrupy.assertion import SnapshotAssertion from syrupy.extensions.image import PNGImageSnapshotExtension -from homeassistant.components.habitica.const import DOMAIN +from homeassistant.components.habitica.const import ASSETS_URL, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -97,3 +98,85 @@ async def test_image_platform( assert (await resp.read()) == snapshot( extension_class=PNGImageSnapshotExtension ) + + +@pytest.mark.usefixtures("habitica") +@respx.mock +async def test_load_image_from_url( + hass: HomeAssistant, + config_entry: MockConfigEntry, + habitica: AsyncMock, + hass_client: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test loading of image from URL.""" + freezer.move_to("2024-09-20T22:00:00.000") + + call1 = respx.get(f"{ASSETS_URL}quest_atom1.png").respond(content=b"\x89PNG") + call2 = respx.get(f"{ASSETS_URL}quest_dustbunnies.png").respond(content=b"\x89PNG") + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert (state := hass.states.get("image.test_user_s_party_quest")) + assert state.state == "2024-09-20T22:00:00+00:00" + + client = await hass_client() + resp = await client.get(state.attributes["entity_picture"]) + + assert resp.status == HTTPStatus.OK + + assert (await resp.read()) == b"\x89PNG" + + assert call1.call_count == 1 + + habitica.get_group.return_value = HabiticaGroupsResponse.from_json( + await async_load_fixture(hass, "party_2.json", DOMAIN) + ) + freezer.tick(timedelta(minutes=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get("image.test_user_s_party_quest")) + assert state.state == "2024-09-20T22:15:00+00:00" + + client = await hass_client() + resp = await client.get(state.attributes["entity_picture"]) + + assert resp.status == HTTPStatus.OK + + assert (await resp.read()) == b"\x89PNG" + assert call2.call_count == 1 + + +@pytest.mark.usefixtures("habitica") +@respx.mock +async def test_load_image_not_found( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test NotFound error.""" + freezer.move_to("2024-09-20T22:00:00.000") + + call1 = respx.get(f"{ASSETS_URL}quest_atom1.png").respond(status_code=404) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert (state := hass.states.get("image.test_user_s_party_quest")) + assert state.state == "2024-09-20T22:00:00+00:00" + + client = await hass_client() + resp = await client.get(state.attributes["entity_picture"]) + + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + + assert call1.call_count == 1 diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index e904ccc890d..92be6cbe881 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -6,11 +6,13 @@ from unittest.mock import AsyncMock from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory +from habiticalib import HabiticaUserResponse import pytest from homeassistant.components.habitica.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from .conftest import ( ERROR_BAD_REQUEST, @@ -19,7 +21,7 @@ from .conftest import ( ERROR_TOO_MANY_REQUESTS, ) -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, async_load_fixture @pytest.mark.usefixtures("habitica") @@ -128,3 +130,41 @@ async def test_coordinator_rate_limited( await hass.async_block_till_done() assert "Rate limit exceeded, will try again later" in caplog.text + + +async def test_remove_party_and_reload( + hass: HomeAssistant, + config_entry: MockConfigEntry, + habitica: AsyncMock, + freezer: FrozenDateTimeFactory, + device_registry: dr.DeviceRegistry, +) -> None: + """Test we leave the party and device is removed.""" + group_id = "1e87097c-4c03-4f8c-a475-67cc7da7f409" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert ( + device_registry.async_get_device( + {(DOMAIN, f"{config_entry.unique_id}_{group_id}")} + ) + is not None + ) + + habitica.get_user.return_value = HabiticaUserResponse.from_json( + await async_load_fixture(hass, "user_no_party.json", DOMAIN) + ) + + freezer.tick(datetime.timedelta(seconds=60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + device_registry.async_get_device( + {(DOMAIN, f"{config_entry.unique_id}_{group_id}")} + ) + is None + ) diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index b0d3920be09..a4ad0a4a004 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -850,3 +850,50 @@ async def test_supervisor_issues_detached_addon_missing( "addon_url": "/hassio/addon/test", }, ) + + +@pytest.mark.usefixtures("all_setup_requests") +async def test_supervisor_issues_disk_lifetime( + hass: HomeAssistant, + supervisor_client: AsyncMock, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test supervisor issue for disk lifetime nearly exceeded.""" + mock_resolution_info(supervisor_client) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "supervisor/event", + "data": { + "event": "issue_changed", + "data": { + "uuid": (issue_uuid := uuid4().hex), + "type": "disk_lifetime", + "context": "system", + "reference": None, + }, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_issue_repair_in_list( + msg["result"]["issues"], + uuid=issue_uuid, + context="system", + type_="disk_lifetime", + fixable=False, + placeholders=None, + ) diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 39f9d4580bd..4234aab40c1 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -180,6 +180,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions( ["system_execute_reboot", "system_execute_reboot"], ["system_test_type", "system_test_type"], ], + "required": False, "name": "next_step_id", } ], @@ -275,6 +276,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir ["system_execute_reboot", "system_execute_reboot"], ["system_test_type", "system_test_type"], ], + "required": False, "name": "next_step_id", } ], @@ -545,6 +547,7 @@ async def test_mount_failed_repair_flow( ["mount_execute_reload", "mount_execute_reload"], ["mount_execute_remove", "mount_execute_remove"], ], + "required": False, "name": "next_step_id", } ], @@ -756,6 +759,7 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks( ["system_rename_data_disk", "system_rename_data_disk"], ["system_adopt_data_disk", "system_adopt_data_disk"], ], + "required": False, "name": "next_step_id", } ], @@ -960,6 +964,7 @@ async def test_supervisor_issue_addon_boot_fail( ["addon_execute_start", "addon_execute_start"], ["addon_disable_boot", "addon_disable_boot"], ], + "required": False, "name": "next_step_id", } ], diff --git a/tests/components/home_connect/test_services.py b/tests/components/home_connect/test_services.py index 33a7f7aee71..645ee1fb08c 100644 --- a/tests/components/home_connect/test_services.py +++ b/tests/components/home_connect/test_services.py @@ -1,11 +1,10 @@ """Tests for the Home Connect actions.""" from collections.abc import Awaitable, Callable -from http import HTTPStatus from typing import Any from unittest.mock import MagicMock -from aiohomeconnect.model import HomeAppliance, OptionKey, ProgramKey, SettingKey +from aiohomeconnect.model import HomeAppliance, SettingKey import pytest from syrupy.assertion import SnapshotAssertion @@ -14,37 +13,10 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr -import homeassistant.helpers.issue_registry as ir from tests.common import MockConfigEntry -from tests.typing import ClientSessionGenerator - -DEPRECATED_SERVICE_KV_CALL_PARAMS = [ - { - "domain": DOMAIN, - "service": "set_option_active", - "service_data": { - "device_id": "DEVICE_ID", - "key": OptionKey.BSH_COMMON_FINISH_IN_RELATIVE.value, - "value": 43200, - "unit": "seconds", - }, - "blocking": True, - }, - { - "domain": DOMAIN, - "service": "set_option_selected", - "service_data": { - "device_id": "DEVICE_ID", - "key": OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE.value, - "value": "LaundryCare.Washer.EnumType.Temperature.GC40", - }, - "blocking": True, - }, -] SERVICE_KV_CALL_PARAMS = [ - *DEPRECATED_SERVICE_KV_CALL_PARAMS, { "domain": DOMAIN, "service": "change_setting", @@ -57,70 +29,13 @@ SERVICE_KV_CALL_PARAMS = [ }, ] -SERVICE_COMMAND_CALL_PARAMS = [ - { - "domain": DOMAIN, - "service": "pause_program", - "service_data": { - "device_id": "DEVICE_ID", - }, - "blocking": True, - }, - { - "domain": DOMAIN, - "service": "resume_program", - "service_data": { - "device_id": "DEVICE_ID", - }, - "blocking": True, - }, -] - - -SERVICE_PROGRAM_CALL_PARAMS = [ - { - "domain": DOMAIN, - "service": "select_program", - "service_data": { - "device_id": "DEVICE_ID", - "program": ProgramKey.LAUNDRY_CARE_WASHER_COTTON.value, - "key": OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE.value, - "value": "LaundryCare.Washer.EnumType.Temperature.GC40", - }, - "blocking": True, - }, - { - "domain": DOMAIN, - "service": "start_program", - "service_data": { - "device_id": "DEVICE_ID", - "program": ProgramKey.LAUNDRY_CARE_WASHER_COTTON.value, - "key": OptionKey.BSH_COMMON_FINISH_IN_RELATIVE.value, - "value": 43200, - "unit": "seconds", - }, - "blocking": True, - }, -] SERVICE_APPLIANCE_METHOD_MAPPING = { - "set_option_active": "set_active_program_option", - "set_option_selected": "set_selected_program_option", "change_setting": "set_setting", - "pause_program": "put_command", - "resume_program": "put_command", - "select_program": "set_selected_program", - "start_program": "start_program", } SERVICE_VALIDATION_ERROR_MAPPING = { - "set_option_active": r"Error.*setting.*options.*active.*program.*", - "set_option_selected": r"Error.*setting.*options.*selected.*program.*", "change_setting": r"Error.*assigning.*value.*setting.*", - "pause_program": r"Error.*executing.*command.*", - "resume_program": r"Error.*executing.*command.*", - "select_program": r"Error.*selecting.*program.*", - "start_program": r"Error.*starting.*program.*", } @@ -171,10 +86,7 @@ SERVICES_SET_PROGRAM_AND_OPTIONS = [ @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) -@pytest.mark.parametrize( - "service_call", - SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, -) +@pytest.mark.parametrize("service_call", SERVICE_KV_CALL_PARAMS) async def test_key_value_services( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -202,81 +114,6 @@ async def test_key_value_services( ) -@pytest.mark.parametrize("appliance", ["Washer"], indirect=True) -@pytest.mark.parametrize( - ("service_call", "issue_id"), - [ - *zip( - DEPRECATED_SERVICE_KV_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, - ["deprecated_set_program_and_option_actions"] - * ( - len(DEPRECATED_SERVICE_KV_CALL_PARAMS) - + len(SERVICE_PROGRAM_CALL_PARAMS) - ), - strict=True, - ), - *zip( - SERVICE_COMMAND_CALL_PARAMS, - ["deprecated_command_actions"] * len(SERVICE_COMMAND_CALL_PARAMS), - strict=True, - ), - ], -) -async def test_programs_and_options_actions_deprecation( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - device_registry: dr.DeviceRegistry, - issue_registry: ir.IssueRegistry, - client: MagicMock, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - appliance: HomeAppliance, - service_call: dict[str, Any], - issue_id: str, -) -> None: - """Test deprecated service keys.""" - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, appliance.ha_id)}, - ) - - service_call["service_data"]["device_id"] = device_entry.id - await hass.services.async_call(**service_call) - await hass.async_block_till_done() - - assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue(DOMAIN, issue_id) - assert issue - - _client = await hass_client() - resp = await _client.post( - "/api/repairs/issues/fix", - json={"handler": DOMAIN, "issue_id": issue.issue_id}, - ) - assert resp.status == HTTPStatus.OK - flow_id = (await resp.json())["flow_id"] - resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}") - - assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 - - await hass.services.async_call(**service_call) - await hass.async_block_till_done() - - assert len(issue_registry.issues) == 1 - assert issue_registry.async_get_issue(DOMAIN, issue_id) - - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - - # Assert the issue is no longer present - assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 - - @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) @pytest.mark.parametrize( ("service_call", "called_method"), @@ -360,7 +197,7 @@ async def test_set_program_and_options_exceptions( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) @pytest.mark.parametrize( "service_call", - SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, + SERVICE_KV_CALL_PARAMS, ) async def test_services_exception_device_id( hass: HomeAssistant, @@ -430,7 +267,7 @@ async def test_services_appliance_not_found( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) @pytest.mark.parametrize( "service_call", - SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, + SERVICE_KV_CALL_PARAMS, ) async def test_services_exception( hass: HomeAssistant, diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 3b075b44356..ea9c638c022 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -363,14 +363,14 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_2576_2580', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'state': dict({ 'attributes': dict({ 'device_class': 'pm25', 'friendly_name': 'Airversa AP2 1808 PM2.5 Density', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'entity_id': 'sensor.airversa_ap2_1808_pm2_5_density', 'state': '3.0', @@ -900,7 +900,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery-20', 'original_name': 'eufyCam2-0000 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -1156,7 +1156,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery-40', 'original_name': 'eufyCam2-000A Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -1412,7 +1412,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery-alert', 'original_name': 'eufyCam2-000A Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -1848,7 +1848,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Contact Sensor Battery Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -2270,7 +2270,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Programmable Switch Battery Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -2601,7 +2601,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery-80', 'original_name': 'ArloBabyA0 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -4473,7 +4473,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Basement Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -4780,7 +4780,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Basement Window 1 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -5037,7 +5037,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Deck Door Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -5294,7 +5294,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Front Door Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -5551,7 +5551,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Garage Door Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -5765,7 +5765,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Living Room Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -6072,7 +6072,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Living Room Window 1 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -6329,7 +6329,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Loft window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -6543,7 +6543,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Master BR Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -6850,7 +6850,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Master BR Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -7462,7 +7462,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Upstairs BR Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -7769,7 +7769,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Upstairs BR Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -10111,7 +10111,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery-60', 'original_name': 'Eve Degree AA11 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -11099,7 +11099,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Family Room North Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -11351,7 +11351,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Kitchen Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -12392,7 +12392,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Laundry Smoke ED78 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -12568,7 +12568,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Family Room North Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -12820,7 +12820,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Kitchen Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -14645,7 +14645,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Laundry Smoke ED78 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -16083,7 +16083,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Hue dimmer switch battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -20820,7 +20820,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'Master Bath South RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -21072,7 +21072,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'RYSE SmartShade RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -21248,7 +21248,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'BR Left RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -21420,7 +21420,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery-90', 'original_name': 'LR Left RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -21592,7 +21592,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery', 'original_name': 'LR Right RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, @@ -21844,7 +21844,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:battery-unknown', + 'original_icon': 'mdi:battery-alert', 'original_name': 'RZSS RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 97856c2c784..868a18af1f9 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -88,3 +88,32 @@ async def test_storage_is_updated_on_add( # Is saved out to store? await flush_store(entity_map.store) assert hkid in hass_storage[ENTITY_MAP]["data"]["pairings"] + + +async def test_storage_is_saved_on_stop( + hass: HomeAssistant, hass_storage: dict[str, Any], get_next_aid: Callable[[], int] +) -> None: + """Test entity map storage is saved when Home Assistant stops.""" + await setup_test_component(hass, get_next_aid(), create_lightbulb_service) + + entity_map: EntityMapStorage = hass.data[ENTITY_MAP] + hkid = "00:00:00:00:00:00" + + # Verify the device is in memory + assert hkid in entity_map.storage_data + + # Clear the storage to verify it gets saved on stop + del hass_storage[ENTITY_MAP] + + # Make a change to trigger a save + entity_map.async_create_or_update_map(hkid, 2, []) # Update config_num + + # Simulate Home Assistant stopping (sets the state and fires the event) + await hass.async_stop() + await hass.async_block_till_done() + + # Verify the storage was saved + assert ENTITY_MAP in hass_storage + assert hkid in hass_storage[ENTITY_MAP]["data"]["pairings"] + # Verify the updated data was saved + assert hass_storage[ENTITY_MAP]["data"]["pairings"][hkid]["config_num"] == 2 diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index ec8406bfe7b..55a2c687867 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -363,6 +363,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -376,6 +377,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -417,6 +419,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -430,6 +433,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -533,6 +537,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -546,6 +551,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -587,6 +593,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -600,6 +607,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index e1b2b2bff61..6d45861b227 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -543,7 +543,14 @@ async def test_get_trigger_capabilities_on(hass: HomeAssistant) -> None: assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + ) == [ + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } + ] async def test_get_trigger_capabilities_off(hass: HomeAssistant) -> None: @@ -563,7 +570,14 @@ async def test_get_trigger_capabilities_off(hass: HomeAssistant) -> None: assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + ) == [ + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } + ] async def test_get_trigger_capabilities_humidity(hass: HomeAssistant) -> None: @@ -588,13 +602,20 @@ async def test_get_trigger_capabilities_humidity(hass: HomeAssistant) -> None: "description": {"suffix": "%"}, "name": "above", "optional": True, + "required": False, "type": "integer", }, { "description": {"suffix": "%"}, "name": "below", "optional": True, + "required": False, "type": "integer", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] diff --git a/tests/components/husqvarna_automower/snapshots/test_event.ambr b/tests/components/husqvarna_automower/snapshots/test_event.ambr new file mode 100644 index 00000000000..e01f8d04f2c --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_event.ambr @@ -0,0 +1,303 @@ +# serializer version: 1 +# name: test_event_snapshot[event.test_mower_1_message-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'alarm_mower_in_motion', + 'alarm_mower_lifted', + 'alarm_mower_stopped', + 'alarm_mower_switched_off', + 'alarm_mower_tilted', + 'alarm_outside_geofence', + 'angular_sensor_problem', + 'battery_problem', + 'battery_restriction_due_to_ambient_temperature', + 'can_error', + 'charging_current_too_high', + 'charging_station_blocked', + 'charging_system_problem', + 'collision_sensor_defect', + 'collision_sensor_error', + 'collision_sensor_problem_front', + 'collision_sensor_problem_rear', + 'com_board_not_available', + 'communication_circuit_board_sw_must_be_updated', + 'complex_working_area', + 'connection_changed', + 'connection_not_changed', + 'connectivity_problem', + 'connectivity_settings_restored', + 'cutting_drive_motor_1_defect', + 'cutting_drive_motor_2_defect', + 'cutting_drive_motor_3_defect', + 'cutting_height_blocked', + 'cutting_height_problem', + 'cutting_height_problem_curr', + 'cutting_height_problem_dir', + 'cutting_height_problem_drive', + 'cutting_motor_problem', + 'cutting_stopped_slope_too_steep', + 'cutting_system_blocked', + 'cutting_system_imbalance_warning', + 'cutting_system_major_imbalance', + 'destination_not_reachable', + 'difficult_finding_home', + 'docking_sensor_defect', + 'electronic_problem', + 'empty_battery', + 'folding_cutting_deck_sensor_defect', + 'folding_sensor_activated', + 'geofence_problem', + 'gps_navigation_problem', + 'guide_1_not_found', + 'guide_2_not_found', + 'guide_3_not_found', + 'guide_calibration_accomplished', + 'guide_calibration_failed', + 'high_charging_power_loss', + 'high_internal_power_loss', + 'high_internal_temperature', + 'internal_voltage_error', + 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_sub_device_combination', + 'invalid_system_configuration', + 'left_brush_motor_overloaded', + 'lift_sensor_defect', + 'lifted', + 'limited_cutting_height_range', + 'loop_sensor_defect', + 'loop_sensor_problem_front', + 'loop_sensor_problem_left', + 'loop_sensor_problem_rear', + 'loop_sensor_problem_right', + 'low_battery', + 'memory_circuit_problem', + 'mower_lifted', + 'mower_tilted', + 'no_accurate_position_from_satellites', + 'no_confirmed_position', + 'no_drive', + 'no_loop_signal', + 'no_power_in_charging_station', + 'no_response_from_charger', + 'outside_working_area', + 'poor_signal_quality', + 'reference_station_communication_problem', + 'right_brush_motor_overloaded', + 'safety_function_faulty', + 'settings_restored', + 'sim_card_locked', + 'sim_card_not_found', + 'sim_card_requires_pin', + 'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern', + 'slope_too_steep', + 'sms_could_not_be_sent', + 'stop_button_problem', + 'stuck_in_charging_station', + 'switch_cord_problem', + 'temporary_battery_problem', + 'tilt_sensor_problem', + 'too_high_discharge_current', + 'too_high_internal_current', + 'trapped', + 'ultrasonic_problem', + 'ultrasonic_sensor_1_defect', + 'ultrasonic_sensor_2_defect', + 'ultrasonic_sensor_3_defect', + 'ultrasonic_sensor_4_defect', + 'unexpected_cutting_height_adj', + 'unexpected_error', + 'upside_down', + 'weak_gps_signal', + 'wheel_drive_problem_left', + 'wheel_drive_problem_rear_left', + 'wheel_drive_problem_rear_right', + 'wheel_drive_problem_right', + 'wheel_motor_blocked_left', + 'wheel_motor_blocked_rear_left', + 'wheel_motor_blocked_rear_right', + 'wheel_motor_blocked_right', + 'wheel_motor_overloaded_left', + 'wheel_motor_overloaded_rear_left', + 'wheel_motor_overloaded_rear_right', + 'wheel_motor_overloaded_right', + 'work_area_not_valid', + 'wrong_loop_signal', + 'wrong_pin_code', + 'zone_generator_problem', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.test_mower_1_message', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Message', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'message', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_message', + 'unit_of_measurement': None, + }) +# --- +# name: test_event_snapshot[event.test_mower_1_message-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'date_time': HAFakeDatetime(2025, 7, 13, 15, 30, tzinfo=datetime.timezone.utc), + 'event_type': 'wheel_motor_overloaded_rear_left', + 'event_types': list([ + 'alarm_mower_in_motion', + 'alarm_mower_lifted', + 'alarm_mower_stopped', + 'alarm_mower_switched_off', + 'alarm_mower_tilted', + 'alarm_outside_geofence', + 'angular_sensor_problem', + 'battery_problem', + 'battery_restriction_due_to_ambient_temperature', + 'can_error', + 'charging_current_too_high', + 'charging_station_blocked', + 'charging_system_problem', + 'collision_sensor_defect', + 'collision_sensor_error', + 'collision_sensor_problem_front', + 'collision_sensor_problem_rear', + 'com_board_not_available', + 'communication_circuit_board_sw_must_be_updated', + 'complex_working_area', + 'connection_changed', + 'connection_not_changed', + 'connectivity_problem', + 'connectivity_settings_restored', + 'cutting_drive_motor_1_defect', + 'cutting_drive_motor_2_defect', + 'cutting_drive_motor_3_defect', + 'cutting_height_blocked', + 'cutting_height_problem', + 'cutting_height_problem_curr', + 'cutting_height_problem_dir', + 'cutting_height_problem_drive', + 'cutting_motor_problem', + 'cutting_stopped_slope_too_steep', + 'cutting_system_blocked', + 'cutting_system_imbalance_warning', + 'cutting_system_major_imbalance', + 'destination_not_reachable', + 'difficult_finding_home', + 'docking_sensor_defect', + 'electronic_problem', + 'empty_battery', + 'folding_cutting_deck_sensor_defect', + 'folding_sensor_activated', + 'geofence_problem', + 'gps_navigation_problem', + 'guide_1_not_found', + 'guide_2_not_found', + 'guide_3_not_found', + 'guide_calibration_accomplished', + 'guide_calibration_failed', + 'high_charging_power_loss', + 'high_internal_power_loss', + 'high_internal_temperature', + 'internal_voltage_error', + 'invalid_battery_combination_invalid_combination_of_different_battery_types', + 'invalid_sub_device_combination', + 'invalid_system_configuration', + 'left_brush_motor_overloaded', + 'lift_sensor_defect', + 'lifted', + 'limited_cutting_height_range', + 'loop_sensor_defect', + 'loop_sensor_problem_front', + 'loop_sensor_problem_left', + 'loop_sensor_problem_rear', + 'loop_sensor_problem_right', + 'low_battery', + 'memory_circuit_problem', + 'mower_lifted', + 'mower_tilted', + 'no_accurate_position_from_satellites', + 'no_confirmed_position', + 'no_drive', + 'no_loop_signal', + 'no_power_in_charging_station', + 'no_response_from_charger', + 'outside_working_area', + 'poor_signal_quality', + 'reference_station_communication_problem', + 'right_brush_motor_overloaded', + 'safety_function_faulty', + 'settings_restored', + 'sim_card_locked', + 'sim_card_not_found', + 'sim_card_requires_pin', + 'slipped_mower_has_slipped_situation_not_solved_with_moving_pattern', + 'slope_too_steep', + 'sms_could_not_be_sent', + 'stop_button_problem', + 'stuck_in_charging_station', + 'switch_cord_problem', + 'temporary_battery_problem', + 'tilt_sensor_problem', + 'too_high_discharge_current', + 'too_high_internal_current', + 'trapped', + 'ultrasonic_problem', + 'ultrasonic_sensor_1_defect', + 'ultrasonic_sensor_2_defect', + 'ultrasonic_sensor_3_defect', + 'ultrasonic_sensor_4_defect', + 'unexpected_cutting_height_adj', + 'unexpected_error', + 'upside_down', + 'weak_gps_signal', + 'wheel_drive_problem_left', + 'wheel_drive_problem_rear_left', + 'wheel_drive_problem_rear_right', + 'wheel_drive_problem_right', + 'wheel_motor_blocked_left', + 'wheel_motor_blocked_rear_left', + 'wheel_motor_blocked_rear_right', + 'wheel_motor_blocked_right', + 'wheel_motor_overloaded_left', + 'wheel_motor_overloaded_rear_left', + 'wheel_motor_overloaded_rear_right', + 'wheel_motor_overloaded_right', + 'work_area_not_valid', + 'wrong_loop_signal', + 'wrong_pin_code', + 'zone_generator_problem', + ]), + 'friendly_name': 'Test Mower 1 Message', + 'latitude': 49.0, + 'longitude': 10.0, + 'severity': , + }), + 'context': , + 'entity_id': 'event.test_mower_1_message', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-06-05T12:00:00.000+00:00', + }) +# --- diff --git a/tests/components/husqvarna_automower/test_event.py b/tests/components/husqvarna_automower/test_event.py new file mode 100644 index 00000000000..6cbfa102976 --- /dev/null +++ b/tests/components/husqvarna_automower/test_event.py @@ -0,0 +1,206 @@ +"""Tests for init module.""" + +from collections.abc import Callable +from copy import deepcopy +from datetime import UTC, datetime +from unittest.mock import AsyncMock, patch + +from aioautomower.model import MowerAttributes, SingleMessageData +from aioautomower.model.model_message import Message, Severity, SingleMessageAttributes +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.event import ATTR_EVENT_TYPE +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry + +from . import setup_integration +from .const import TEST_MOWER_ID + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.freeze_time(datetime(2023, 6, 5, 12)) +async def test_event( + hass: HomeAssistant, + entity_registry: EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + values: dict[str, MowerAttributes], +) -> None: + """Test that a new message arriving over the websocket creates and updates the sensor.""" + callbacks: list[Callable[[SingleMessageData], None]] = [] + + @callback + def fake_register_websocket_response( + cb: Callable[[SingleMessageData], None], + ) -> None: + callbacks.append(cb) + + mock_automower_client.register_single_message_callback.side_effect = ( + fake_register_websocket_response + ) + + # Set up integration + await setup_integration(hass, mock_config_entry) + await hass.async_block_till_done() + + # Ensure callback was registered for the test mower + assert mock_automower_client.register_single_message_callback.called + + # Check initial state (event entity not available yet) + state = hass.states.get("event.test_mower_1_message") + assert state is None + + # Simulate a new message for this mower and check entity creation + message = SingleMessageData( + type="messages", + id=TEST_MOWER_ID, + attributes=SingleMessageAttributes( + message=Message( + time=datetime(2025, 7, 13, 15, 30, tzinfo=UTC), + code="wheel_motor_overloaded_rear_left", + severity=Severity.ERROR, + latitude=49.0, + longitude=10.0, + ) + ), + ) + + for cb in callbacks: + cb(message) + await hass.async_block_till_done() + state = hass.states.get("event.test_mower_1_message") + assert state is not None + assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left" + + # Reload the config entry to ensure the entity is created again + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.LOADED + state = hass.states.get("event.test_mower_1_message") + assert state is not None + assert state.attributes[ATTR_EVENT_TYPE] == "wheel_motor_overloaded_rear_left" + + # Check updating event with a new message + message = SingleMessageData( + type="messages", + id=TEST_MOWER_ID, + attributes=SingleMessageAttributes( + message=Message( + time=datetime(2025, 7, 13, 16, 00, tzinfo=UTC), + code="alarm_mower_lifted", + severity=Severity.ERROR, + latitude=48.0, + longitude=11.0, + ) + ), + ) + + for cb in callbacks: + cb(message) + await hass.async_block_till_done() + state = hass.states.get("event.test_mower_1_message") + assert state is not None + assert state.attributes[ATTR_EVENT_TYPE] == "alarm_mower_lifted" + + # Check message for another mower, creates an new entity and dont + # change the state of the first entity + message = SingleMessageData( + type="messages", + id="1234", + attributes=SingleMessageAttributes( + message=Message( + time=datetime(2025, 7, 13, 16, 00, tzinfo=UTC), + code="battery_problem", + severity=Severity.ERROR, + latitude=48.0, + longitude=11.0, + ) + ), + ) + + for cb in callbacks: + cb(message) + await hass.async_block_till_done() + entry = entity_registry.async_get("event.test_mower_1_message") + assert entry is not None + assert state.attributes[ATTR_EVENT_TYPE] == "alarm_mower_lifted" + state = hass.states.get("event.test_mower_2_message") + assert state is not None + assert state.attributes[ATTR_EVENT_TYPE] == "battery_problem" + + # Check event entity is removed, when the mower is removed + values_copy = deepcopy(values) + values_copy.pop("1234") + mock_automower_client.get_status.return_value = values_copy + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("event.test_mower_2_message") + assert state is None + entry = entity_registry.async_get("event.test_mower_2_message") + assert entry is None + + +@pytest.mark.freeze_time(datetime(2023, 6, 5, 12)) +async def test_event_snapshot( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test that a new message arriving over the websocket updates the sensor.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.EVENT], + ): + callbacks: list[Callable[[SingleMessageData], None]] = [] + + @callback + def fake_register_websocket_response( + cb: Callable[[SingleMessageData], None], + ) -> None: + callbacks.append(cb) + + mock_automower_client.register_single_message_callback.side_effect = ( + fake_register_websocket_response + ) + + # Set up integration + await setup_integration(hass, mock_config_entry) + await hass.async_block_till_done() + + # Ensure callback was registered for the test mower + assert mock_automower_client.register_single_message_callback.called + + # Simulate a new message for this mower + message = SingleMessageData( + type="messages", + id=TEST_MOWER_ID, + attributes=SingleMessageAttributes( + message=Message( + time=datetime(2025, 7, 13, 15, 30, tzinfo=UTC), + code="wheel_motor_overloaded_rear_left", + severity=Severity.ERROR, + latitude=49.0, + longitude=10.0, + ) + ), + ) + + for cb in callbacks: + cb(message) + await hass.async_block_till_done() + + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) diff --git a/tests/components/husqvarna_automower_ble/conftest.py b/tests/components/husqvarna_automower_ble/conftest.py index 3a8e881aba0..1081db014e3 100644 --- a/tests/components/husqvarna_automower_ble/conftest.py +++ b/tests/components/husqvarna_automower_ble/conftest.py @@ -3,6 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch +from automower_ble.protocol import ResponseResult import pytest from homeassistant.components.husqvarna_automower_ble.const import DOMAIN @@ -37,7 +38,7 @@ def mock_automower_client(enable_bluetooth: None) -> Generator[AsyncMock]: ), ): client = mock_client.return_value - client.connect.return_value = True + client.connect.return_value = ResponseResult.OK client.is_connected.return_value = True client.get_model.return_value = "305" client.battery_level.return_value = 100 diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr index 6b4ab8236f9..2e7369e8a6d 100644 --- a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr @@ -1,11 +1,15 @@ # serializer version: 1 # name: test_setup DeviceRegistryEntrySnapshot({ - 'area_id': None, + 'area_id': 'garden', 'config_entries': , 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ + tuple( + 'bluetooth', + '00000000-0000-0000-0000-000000000003', + ), }), 'disabled_by': None, 'entry_type': None, diff --git a/tests/components/husqvarna_automower_ble/test_init.py b/tests/components/husqvarna_automower_ble/test_init.py index 3cb4338eca4..95a0a1f2037 100644 --- a/tests/components/husqvarna_automower_ble/test_init.py +++ b/tests/components/husqvarna_automower_ble/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import Mock -from bleak import BleakError +from automower_ble.protocol import ResponseResult import pytest from syrupy.assertion import SnapshotAssertion @@ -46,7 +46,7 @@ async def test_setup_retry_connect( ) -> None: """Test setup creates expected devices.""" - mock_automower_client.connect.return_value = False + mock_automower_client.connect.side_effect = TimeoutError mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -55,14 +55,13 @@ async def test_setup_retry_connect( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY -async def test_setup_failed_connect( +async def test_setup_unknown_error( hass: HomeAssistant, mock_automower_client: Mock, mock_config_entry: MockConfigEntry, ) -> None: - """Test setup creates expected devices.""" - - mock_automower_client.connect.side_effect = BleakError + """Test setup fails when we receive an error from the device.""" + mock_automower_client.connect.return_value = ResponseResult.UNKNOWN_ERROR mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/husqvarna_automower_ble/test_lawn_mower.py b/tests/components/husqvarna_automower_ble/test_lawn_mower.py index 3f00d3dbff0..2a127c785d9 100644 --- a/tests/components/husqvarna_automower_ble/test_lawn_mower.py +++ b/tests/components/husqvarna_automower_ble/test_lawn_mower.py @@ -3,10 +3,12 @@ from datetime import timedelta from unittest.mock import Mock +from automower_ble.protocol import MowerActivity, MowerState from bleak import BleakError from freezegun.api import FrozenDateTimeFactory import pytest +from homeassistant.components.lawn_mower import LawnMowerActivity from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -124,3 +126,83 @@ async def test_bleak_error_data_update( await hass.async_block_till_done() assert hass.states.get("lawn_mower.husqvarna_automower").state == STATE_UNAVAILABLE + + +OPERATIONAL_STATES = [ + MowerState.IN_OPERATION, + MowerState.PENDING_START, + MowerState.RESTRICTED, +] + + +@pytest.mark.parametrize( + ("mower_states", "mower_activities", "expected_state"), + [ + # MowerState ERROR, FATAL_ERROR, OFF, STOPPED, WAIT_FOR_SAFETYPIN -> Mapped to + # LawnMowerActivity.ERROR + ( + [ + MowerState.ERROR, + MowerState.FATAL_ERROR, + MowerState.OFF, + MowerState.STOPPED, + MowerState.WAIT_FOR_SAFETYPIN, + ], + list(MowerActivity), + LawnMowerActivity.ERROR, + ), + # MowerState PAUSED -> Mapped to LawnMowerActivity.PAUSED + ([MowerState.PAUSED], list(MowerActivity), LawnMowerActivity.PAUSED), + # Operational states are mapped according to the activity + ( + OPERATIONAL_STATES, + [MowerActivity.CHARGING, MowerActivity.NONE, MowerActivity.PARKED], + LawnMowerActivity.DOCKED, + ), + ( + OPERATIONAL_STATES, + [MowerActivity.GOING_OUT, MowerActivity.MOWING], + LawnMowerActivity.MOWING, + ), + ( + OPERATIONAL_STATES, + [MowerActivity.GOING_HOME], + LawnMowerActivity.RETURNING, + ), + ( + OPERATIONAL_STATES, + [MowerActivity.STOPPED_IN_GARDEN], + LawnMowerActivity.ERROR, + ), + ], +) +async def test_mower_activity_mapping( + hass: HomeAssistant, + mock_automower_client: Mock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + mower_states: list[MowerState], + mower_activities: list[MowerActivity], + expected_state: str, +) -> None: + """Test mower state and activity mapping to LawnMowerActivity states.""" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + for mower_state in mower_states: + for mower_activity in mower_activities: + mock_automower_client.mower_state.return_value = mower_state + mock_automower_client.mower_activity.return_value = mower_activity + + freezer.tick(timedelta(seconds=60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + hass.states.get("lawn_mower.husqvarna_automower").state + == expected_state + ) diff --git a/tests/components/huum/snapshots/test_number.ambr b/tests/components/huum/snapshots/test_number.ambr new file mode 100644 index 00000000000..19c0642f007 --- /dev/null +++ b/tests/components/huum/snapshots/test_number.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_number_entity[number.huum_sauna_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.huum_sauna_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'huum', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'AABBCC112233', + 'unit_of_measurement': None, + }) +# --- +# name: test_number_entity[number.huum_sauna_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Huum sauna Humidity', + 'max': 10, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.huum_sauna_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- diff --git a/tests/components/huum/test_number.py b/tests/components/huum/test_number.py new file mode 100644 index 00000000000..3d7a74bfce3 --- /dev/null +++ b/tests/components/huum/test_number.py @@ -0,0 +1,77 @@ +"""Tests for the Huum number entity.""" + +from unittest.mock import AsyncMock + +from huum.const import SaunaStatus +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_with_selected_platforms + +from tests.common import MockConfigEntry, snapshot_platform + +ENTITY_ID = "number.huum_sauna_humidity" + + +async def test_number_entity( + hass: HomeAssistant, + mock_huum: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test the initial parameters.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER]) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_set_humidity( + hass: HomeAssistant, + mock_huum: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting the humidity.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER]) + + mock_huum.status = SaunaStatus.ONLINE_HEATING + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_VALUE: 5, + }, + blocking=True, + ) + + mock_huum.turn_on.assert_called_once_with(temperature=80, humidity=5) + + +async def test_dont_set_humidity_when_sauna_not_heating( + hass: HomeAssistant, + mock_huum: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting the humidity.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER]) + + mock_huum.status = SaunaStatus.ONLINE_NOT_HEATING + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_VALUE: 5, + }, + blocking=True, + ) + + mock_huum.turn_on.assert_not_called() diff --git a/tests/components/imeon_inverter/conftest.py b/tests/components/imeon_inverter/conftest.py index e147a6ff642..37fa47e7afb 100644 --- a/tests/components/imeon_inverter/conftest.py +++ b/tests/components/imeon_inverter/conftest.py @@ -66,7 +66,7 @@ def mock_imeon_inverter() -> Generator[MagicMock]: "serial": TEST_SERIAL, "url": f"http://{TEST_USER_INPUT[CONF_HOST]}", } - inverter.storage = load_json_object_fixture("sensor_data.json", DOMAIN) + inverter.storage = load_json_object_fixture("entity_data.json", DOMAIN) yield inverter diff --git a/tests/components/imeon_inverter/fixtures/entity_data.json b/tests/components/imeon_inverter/fixtures/entity_data.json new file mode 100644 index 00000000000..a2717f093f4 --- /dev/null +++ b/tests/components/imeon_inverter/fixtures/entity_data.json @@ -0,0 +1,79 @@ +{ + "battery": { + "battery_power": 2500.0, + "battery_soc": 78.0, + "battery_status": "charging", + "battery_stored": 10200.0, + "battery_consumed": 500.0 + }, + "grid": { + "grid_current_l1": 12.5, + "grid_current_l2": 10.8, + "grid_current_l3": 11.2, + "grid_frequency": 50.0, + "grid_voltage_l1": 230.0, + "grid_voltage_l2": 229.5, + "grid_voltage_l3": 230.1 + }, + "input": { + "input_power_l1": 1000.0, + "input_power_l2": 950.0, + "input_power_l3": 980.0, + "input_power_total": 2930.0 + }, + "inverter": { + "inverter_charging_current_limit": 50, + "inverter_injection_power_limit": 5000.0, + "manager_inverter_state": "grid_consumption" + }, + "meter": { + "meter_power": 2000.0 + }, + "output": { + "output_current_l1": 15.0, + "output_current_l2": 14.5, + "output_current_l3": 15.2, + "output_frequency": 49.9, + "output_power_l1": 1100.0, + "output_power_l2": 1080.0, + "output_power_l3": 1120.0, + "output_power_total": 3300.0, + "output_voltage_l1": 231.0, + "output_voltage_l2": 229.8, + "output_voltage_l3": 230.2 + }, + "pv": { + "pv_consumed": 1500.0, + "pv_injected": 800.0, + "pv_power_1": 1200.0, + "pv_power_2": 1300.0, + "pv_power_total": 2500.0 + }, + "temp": { + "temp_air_temperature": 25.0, + "temp_component_temperature": 45.5 + }, + "monitoring": { + "monitoring_self_produced": 2600.0, + "monitoring_self_consumption": 85.0, + "monitoring_self_sufficiency": 90.0 + }, + "monitoring_minute": { + "monitoring_minute_building_consumption": 50.0, + "monitoring_minute_grid_consumption": 8.3, + "monitoring_minute_grid_injection": 11.7, + "monitoring_minute_grid_power_flow": -3.4, + "monitoring_minute_solar_production": 43.3 + }, + "timeline": { + "timeline_type_msg": "info_bat" + }, + "energy": { + "energy_pv": 12000.0, + "energy_grid_injected": 5000.0, + "energy_grid_consumed": 6000.0, + "energy_building_consumption": 15000.0, + "energy_battery_stored": 8000.0, + "energy_battery_consumed": 2000.0 + } +} diff --git a/tests/components/imeon_inverter/fixtures/sensor_data.json b/tests/components/imeon_inverter/fixtures/sensor_data.json deleted file mode 100644 index 566716fe3fa..00000000000 --- a/tests/components/imeon_inverter/fixtures/sensor_data.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "battery": { - "autonomy": 4.5, - "charge_time": 120, - "power": 2500.0, - "soc": 78.0, - "stored": 10.2 - }, - "grid": { - "current_l1": 12.5, - "current_l2": 10.8, - "current_l3": 11.2, - "frequency": 50.0, - "voltage_l1": 230.0, - "voltage_l2": 229.5, - "voltage_l3": 230.1 - }, - "input": { - "power_l1": 1000.0, - "power_l2": 950.0, - "power_l3": 980.0, - "power_total": 2930.0 - }, - "inverter": { - "charging_current_limit": 50, - "injection_power_limit": 5000.0 - }, - "meter": { - "power": 2000.0, - "power_protocol": 2018.0 - }, - "output": { - "current_l1": 15.0, - "current_l2": 14.5, - "current_l3": 15.2, - "frequency": 49.9, - "power_l1": 1100.0, - "power_l2": 1080.0, - "power_l3": 1120.0, - "power_total": 3300.0, - "voltage_l1": 231.0, - "voltage_l2": 229.8, - "voltage_l3": 230.2 - }, - "pv": { - "consumed": 1500.0, - "injected": 800.0, - "power_1": 1200.0, - "power_2": 1300.0, - "power_total": 2500.0 - }, - "temp": { - "air_temperature": 25.0, - "component_temperature": 45.5 - }, - "monitoring": { - "building_consumption": 3000.0, - "economy_factor": 0.8, - "grid_consumption": 500.0, - "grid_injection": 700.0, - "grid_power_flow": -200.0, - "self_consumption": 85.0, - "self_sufficiency": 90.0, - "solar_production": 2600.0 - }, - "monitoring_minute": { - "building_consumption": 50.0, - "grid_consumption": 8.3, - "grid_injection": 11.7, - "grid_power_flow": -3.4, - "solar_production": 43.3 - } -} diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index fb59aa9dede..673f561d540 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -52,7 +52,63 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '25.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_consumed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_battery_consumed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery consumed', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_consumed', + 'unique_id': '111111111111111_battery_consumed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_consumed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Battery consumed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_battery_consumed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_battery_power-entry] @@ -108,7 +164,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2500.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_battery_state_of_charge-entry] @@ -161,7 +217,67 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '78.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging', + 'discharging', + 'charged', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_battery_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery status', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_status', + 'unique_id': '111111111111111_battery_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Imeon inverter Battery status', + 'options': list([ + 'charging', + 'discharging', + 'charged', + ]), + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_battery_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_battery_stored-entry] @@ -192,7 +308,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'Battery stored', 'platform': 'imeon_inverter', @@ -201,23 +317,79 @@ 'supported_features': 0, 'translation_key': 'battery_stored', 'unique_id': '111111111111111_battery_stored', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_battery_stored-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy_storage', + 'device_class': 'power', 'friendly_name': 'Imeon inverter Battery stored', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_battery_stored', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10.2', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_building_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_building_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Building consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_building_consumption', + 'unique_id': '111111111111111_monitoring_minute_building_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_building_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Building consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_building_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_charging_current_limit-entry] @@ -273,7 +445,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_component_temperature-entry] @@ -329,7 +501,63 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '45.5', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_grid_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_grid_consumption', + 'unique_id': '111111111111111_monitoring_minute_grid_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Grid consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_grid_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_current_l1-entry] @@ -385,7 +613,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '12.5', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_current_l2-entry] @@ -441,7 +669,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10.8', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_current_l3-entry] @@ -497,7 +725,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '11.2', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_frequency-entry] @@ -553,7 +781,119 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_injection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_grid_injection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid injection', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_grid_injection', + 'unique_id': '111111111111111_monitoring_minute_grid_injection', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_injection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Grid injection', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_grid_injection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_power_flow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_grid_power_flow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid power flow', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_grid_power_flow', + 'unique_id': '111111111111111_monitoring_minute_grid_power_flow', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_power_flow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Grid power flow', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_grid_power_flow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_voltage_l1-entry] @@ -609,7 +949,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '230.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_voltage_l2-entry] @@ -665,7 +1005,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '229.5', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_voltage_l3-entry] @@ -721,7 +1061,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '230.1', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_injection_power_limit-entry] @@ -777,7 +1117,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5000.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_input_power_l1-entry] @@ -833,7 +1173,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1000.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_input_power_l2-entry] @@ -889,7 +1229,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '950.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_input_power_l3-entry] @@ -945,7 +1285,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '980.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_input_power_total-entry] @@ -1001,7 +1341,69 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2930.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_inverter_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'unsynchronized', + 'grid_consumption', + 'grid_injection', + 'grid_synchronised_but_not_used', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_inverter_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Inverter state', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'manager_inverter_state', + 'unique_id': '111111111111111_manager_inverter_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.imeon_inverter_inverter_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Imeon inverter Inverter state', + 'options': list([ + 'unsynchronized', + 'grid_consumption', + 'grid_injection', + 'grid_synchronised_but_not_used', + ]), + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_inverter_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_meter_power-entry] @@ -1057,397 +1459,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2000.0', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_building_consumption_minute-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_building_consumption_minute', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Monitoring building consumption (minute)', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'monitoring_minute_building_consumption', - 'unique_id': '111111111111111_monitoring_minute_building_consumption', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_building_consumption_minute-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring building consumption (minute)', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_building_consumption_minute', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '50.0', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_consumption_minute-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_consumption_minute', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Monitoring grid consumption (minute)', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'monitoring_minute_grid_consumption', - 'unique_id': '111111111111111_monitoring_minute_grid_consumption', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_consumption_minute-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring grid consumption (minute)', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_consumption_minute', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8.3', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_injection_minute-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_injection_minute', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Monitoring grid injection (minute)', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'monitoring_minute_grid_injection', - 'unique_id': '111111111111111_monitoring_minute_grid_injection', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_injection_minute-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring grid injection (minute)', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_injection_minute', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '11.7', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_power_flow_minute-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_power_flow_minute', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Monitoring grid power flow (minute)', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'monitoring_minute_grid_power_flow', - 'unique_id': '111111111111111_monitoring_minute_grid_power_flow', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_power_flow_minute-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring grid power flow (minute)', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_power_flow_minute', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '-3.4', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_consumption-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_consumption', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Monitoring self-consumption', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'monitoring_self_consumption', - 'unique_id': '111111111111111_monitoring_self_consumption', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_consumption-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Imeon inverter Monitoring self-consumption', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_consumption', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '85.0', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_sufficiency-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_sufficiency', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Monitoring self-sufficiency', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'monitoring_self_sufficiency', - 'unique_id': '111111111111111_monitoring_self_sufficiency', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_sufficiency-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Imeon inverter Monitoring self-sufficiency', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_sufficiency', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '90.0', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_solar_production_minute-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_solar_production_minute', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Monitoring solar production (minute)', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'monitoring_minute_solar_production', - 'unique_id': '111111111111111_monitoring_minute_solar_production', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_monitoring_solar_production_minute-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring solar production (minute)', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_solar_production_minute', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '43.3', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_current_l1-entry] @@ -1503,7 +1515,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '15.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_current_l2-entry] @@ -1559,7 +1571,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '14.5', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_current_l3-entry] @@ -1615,7 +1627,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '15.2', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_frequency-entry] @@ -1671,7 +1683,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '49.9', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_power_l1-entry] @@ -1727,7 +1739,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1100.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_power_l2-entry] @@ -1783,7 +1795,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1080.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_power_l3-entry] @@ -1839,7 +1851,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1120.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_power_total-entry] @@ -1895,7 +1907,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3300.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_voltage_l1-entry] @@ -1951,7 +1963,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '231.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_voltage_l2-entry] @@ -2007,7 +2019,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '229.8', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_voltage_l3-entry] @@ -2063,7 +2075,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '230.2', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_consumed-entry] @@ -2072,7 +2084,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2094,7 +2106,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'PV consumed', 'platform': 'imeon_inverter', @@ -2103,23 +2115,23 @@ 'supported_features': 0, 'translation_key': 'pv_consumed', 'unique_id': '111111111111111_pv_consumed', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_pv_consumed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy', + 'device_class': 'power', 'friendly_name': 'Imeon inverter PV consumed', - 'state_class': , - 'unit_of_measurement': , + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_pv_consumed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1500.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_injected-entry] @@ -2128,7 +2140,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2150,7 +2162,7 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'PV injected', 'platform': 'imeon_inverter', @@ -2159,23 +2171,23 @@ 'supported_features': 0, 'translation_key': 'pv_injected', 'unique_id': '111111111111111_pv_injected', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.imeon_inverter_pv_injected-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'energy', + 'device_class': 'power', 'friendly_name': 'Imeon inverter PV injected', - 'state_class': , - 'unit_of_measurement': , + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.imeon_inverter_pv_injected', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '800.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_power_1-entry] @@ -2231,7 +2243,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1200.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_power_2-entry] @@ -2287,7 +2299,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1300.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_power_total-entry] @@ -2343,6 +2355,590 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2500.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_self_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Self-consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_self_consumption', + 'unique_id': '111111111111111_monitoring_self_consumption', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Imeon inverter Self-consumption', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_self_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_sufficiency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_self_sufficiency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Self-sufficiency', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_self_sufficiency', + 'unique_id': '111111111111111_monitoring_self_sufficiency', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_sufficiency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Imeon inverter Self-sufficiency', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_self_sufficiency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_solar_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_solar_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar production', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_solar_production', + 'unique_id': '111111111111111_monitoring_minute_solar_production', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_solar_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Solar production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_solar_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_timeline_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'com_lost', + 'warning_grid', + 'warning_pv', + 'warning_bat', + 'error_ond', + 'error_soft', + 'error_pv', + 'error_grid', + 'error_bat', + 'good_1', + 'info_soft', + 'info_ond', + 'info_bat', + 'info_smartlo', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_timeline_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Timeline status', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'timeline_type_msg', + 'unique_id': '111111111111111_timeline_type_msg', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.imeon_inverter_timeline_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Imeon inverter Timeline status', + 'options': list([ + 'com_lost', + 'warning_grid', + 'warning_pv', + 'warning_bat', + 'error_ond', + 'error_soft', + 'error_pv', + 'error_grid', + 'error_bat', + 'good_1', + 'info_soft', + 'info_ond', + 'info_bat', + 'info_smartlo', + ]), + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_timeline_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_consumed_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_battery_consumed_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today battery-consumed energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_battery_consumed', + 'unique_id': '111111111111111_energy_battery_consumed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_consumed_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today battery-consumed energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_battery_consumed_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_stored_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_battery_stored_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today battery-stored energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_battery_stored', + 'unique_id': '111111111111111_energy_battery_stored', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_stored_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today battery-stored energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_battery_stored_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_building_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_building_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today building consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_building_consumption', + 'unique_id': '111111111111111_energy_building_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_building_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today building consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_building_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_consumed_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_grid_consumed_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today grid-consumed energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_grid_consumed', + 'unique_id': '111111111111111_energy_grid_consumed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_consumed_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today grid-consumed energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_grid_consumed_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_injected_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_grid_injected_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today grid-injected energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_grid_injected', + 'unique_id': '111111111111111_energy_grid_injected', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_injected_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today grid-injected energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_grid_injected_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_pv_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_pv_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today PV energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_pv', + 'unique_id': '111111111111111_energy_pv', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_pv_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today PV energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_pv_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- diff --git a/tests/components/imeon_inverter/test_sensor.py b/tests/components/imeon_inverter/test_sensor.py index 194864a67a2..ec50594f6ba 100644 --- a/tests/components/imeon_inverter/test_sensor.py +++ b/tests/components/imeon_inverter/test_sensor.py @@ -1,16 +1,20 @@ """Test the Imeon Inverter sensors.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch +from aiohttp import ClientError +from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.imeon_inverter.coordinator import INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_sensors( @@ -24,3 +28,51 @@ async def test_sensors( await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "exception", + [ + TimeoutError, + ClientError, + ValueError, + ], +) +@pytest.mark.asyncio +async def test_sensor_unavailable_on_update_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_imeon_inverter: MagicMock, + freezer: FrozenDateTimeFactory, + exception: Exception, +) -> None: + """Test that sensor becomes unavailable when update raises an error.""" + entity_id = "sensor.imeon_inverter_battery_power" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE + + mock_imeon_inverter.update.side_effect = exception + + freezer.tick(INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + mock_imeon_inverter.update.side_effect = None + + freezer.tick(INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr index 52fd6bb2ce4..377d29f4a71 100644 --- a/tests/components/iron_os/snapshots/test_number.ambr +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -94,7 +94,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_calibration_offset', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_state[number.pinecil_calibration_offset-state] @@ -105,7 +105,7 @@ 'min': 100, 'mode': , 'step': 1, - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'number.pinecil_calibration_offset', diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr index 39dda49d313..caab12d4120 100644 --- a/tests/components/iron_os/snapshots/test_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -566,7 +566,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.pinecil_raw_tip_voltage-state] @@ -575,7 +575,7 @@ 'device_class': 'voltage', 'friendly_name': 'Pinecil Raw tip voltage', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pinecil_raw_tip_voltage', diff --git a/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr index 0a392e101c5..859cdefd9c2 100644 --- a/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr +++ b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr @@ -3,6 +3,15 @@ dict({ 'data': dict({ 'candle_lighting_offset': 40, + 'dateinfo': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), 'diaspora': False, 'havdalah_offset': 0, 'language': 'en', @@ -17,33 +26,22 @@ 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", }), }), - 'results': dict({ - 'dateinfo': dict({ - 'date': dict({ - 'day': 21, - 'month': 10, - 'year': 5785, - }), - 'diaspora': False, - 'nusach': 'sephardi', + 'zmanim': dict({ + 'candle_lighting_offset': 40, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', }), - 'zmanim': dict({ - 'candle_lighting_offset': 40, - 'date': dict({ - '__type': "", - 'isoformat': '2025-05-19', - }), - 'havdalah_offset': 0, - 'location': dict({ - 'altitude': '**REDACTED**', - 'diaspora': False, - 'latitude': '**REDACTED**', - 'longitude': '**REDACTED**', - 'name': 'test home', - 'timezone': dict({ - '__type': "", - 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", - }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", }), }), }), @@ -59,6 +57,15 @@ dict({ 'data': dict({ 'candle_lighting_offset': 18, + 'dateinfo': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': True, + 'nusach': 'sephardi', + }), 'diaspora': True, 'havdalah_offset': 0, 'language': 'en', @@ -73,33 +80,22 @@ 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", }), }), - 'results': dict({ - 'dateinfo': dict({ - 'date': dict({ - 'day': 21, - 'month': 10, - 'year': 5785, - }), - 'diaspora': True, - 'nusach': 'sephardi', + 'zmanim': dict({ + 'candle_lighting_offset': 18, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', }), - 'zmanim': dict({ - 'candle_lighting_offset': 18, - 'date': dict({ - '__type': "", - 'isoformat': '2025-05-19', - }), - 'havdalah_offset': 0, - 'location': dict({ - 'altitude': '**REDACTED**', - 'diaspora': True, - 'latitude': '**REDACTED**', - 'longitude': '**REDACTED**', - 'name': 'test home', - 'timezone': dict({ - '__type': "", - 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", - }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': True, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", }), }), }), @@ -115,6 +111,15 @@ dict({ 'data': dict({ 'candle_lighting_offset': 18, + 'dateinfo': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), 'diaspora': False, 'havdalah_offset': 0, 'language': 'en', @@ -129,33 +134,22 @@ 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", }), }), - 'results': dict({ - 'dateinfo': dict({ - 'date': dict({ - 'day': 21, - 'month': 10, - 'year': 5785, - }), - 'diaspora': False, - 'nusach': 'sephardi', + 'zmanim': dict({ + 'candle_lighting_offset': 18, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', }), - 'zmanim': dict({ - 'candle_lighting_offset': 18, - 'date': dict({ - '__type': "", - 'isoformat': '2025-05-19', - }), - 'havdalah_offset': 0, - 'location': dict({ - 'altitude': '**REDACTED**', - 'diaspora': False, - 'latitude': '**REDACTED**', - 'longitude': '**REDACTED**', - 'name': 'test home', - 'timezone': dict({ - '__type': "", - 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", - }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", }), }), }), diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index cc3a7a88285..d581f230dde 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -132,7 +132,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.justnimbus.config_flow.justnimbus.JustNimbusClient.get_data", - return_value=MagicMock(), + return_value=MagicMock(api_version="1.0.0"), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/knx/snapshots/test_websocket.ambr b/tests/components/knx/snapshots/test_websocket.ambr new file mode 100644 index 00000000000..b99196c8769 --- /dev/null +++ b/tests/components/knx/snapshots/test_websocket.ambr @@ -0,0 +1,801 @@ +# serializer version: 1 +# name: test_knx_get_schema[binary_sensor] + dict({ + 'id': 1, + 'result': list([ + dict({ + 'collapsible': False, + 'name': 'section_binary_sensor', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_sensor', + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': True, + }), + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), + 'write': False, + }), + 'required': True, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'invert', + 'optional': True, + 'required': False, + 'selector': dict({ + 'boolean': dict({ + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'collapsible': True, + 'name': 'section_advanced_options', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ignore_internal_state', + 'optional': True, + 'required': False, + 'selector': dict({ + 'boolean': dict({ + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'name': 'context_timeout', + 'optional': True, + 'required': False, + 'selector': dict({ + 'number': dict({ + 'max': 10.0, + 'min': 0.0, + 'mode': 'slider', + 'step': 0.1, + 'unit_of_measurement': 's', + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'name': 'reset_after', + 'optional': True, + 'required': False, + 'selector': dict({ + 'number': dict({ + 'max': 600.0, + 'min': 0.0, + 'mode': 'slider', + 'step': 0.1, + 'unit_of_measurement': 's', + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'allow_false': False, + 'default': True, + 'name': 'sync_state', + 'required': True, + 'type': 'knx_sync_state', + }), + ]), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_knx_get_schema[cover] + dict({ + 'id': 1, + 'result': list([ + dict({ + 'collapsible': False, + 'name': 'section_binary_control', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_up_down', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': False, + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'invert_updown', + 'optional': True, + 'required': False, + 'selector': dict({ + 'boolean': dict({ + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'collapsible': False, + 'name': 'section_stop_control', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_stop', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': False, + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'ga_step', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': False, + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'collapsible': True, + 'name': 'section_position_control', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_position_set', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': False, + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'ga_position_state', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'write': False, + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'invert_position', + 'optional': True, + 'required': False, + 'selector': dict({ + 'boolean': dict({ + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'collapsible': True, + 'name': 'section_tilt_control', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_angle', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'invert_angle', + 'optional': True, + 'required': False, + 'selector': dict({ + 'boolean': dict({ + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'collapsible': False, + 'name': 'section_travel_time', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'default': 25, + 'name': 'travelling_time_up', + 'optional': True, + 'required': False, + 'selector': dict({ + 'number': dict({ + 'max': 1000.0, + 'min': 0.0, + 'mode': 'slider', + 'step': 0.1, + 'unit_of_measurement': 's', + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'default': 25, + 'name': 'travelling_time_down', + 'optional': True, + 'required': False, + 'selector': dict({ + 'number': dict({ + 'max': 1000.0, + 'min': 0.0, + 'mode': 'slider', + 'step': 0.1, + 'unit_of_measurement': 's', + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'allow_false': False, + 'default': True, + 'name': 'sync_state', + 'optional': True, + 'required': False, + 'type': 'knx_sync_state', + }), + ]), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_knx_get_schema[light] + dict({ + 'id': 1, + 'result': list([ + dict({ + 'collapsible': False, + 'name': 'section_switch', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_switch', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'collapsible': False, + 'name': 'section_brightness', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_brightness', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'collapsible': True, + 'name': 'section_color_temp', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_color_temp', + 'optional': True, + 'options': dict({ + 'dptSelect': list([ + dict({ + 'dpt': dict({ + 'main': 7, + 'sub': 600, + }), + 'translation_key': '7_600', + 'value': '7.600', + }), + dict({ + 'dpt': dict({ + 'main': 9, + 'sub': None, + }), + 'translation_key': '9', + 'value': '9', + }), + dict({ + 'dpt': dict({ + 'main': 5, + 'sub': 1, + }), + 'translation_key': '5_001', + 'value': '5.001', + }), + ]), + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'write': dict({ + 'required': True, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'default': 2700, + 'name': 'color_temp_min', + 'required': True, + 'selector': dict({ + 'number': dict({ + 'max': 10000.0, + 'min': 1.0, + 'mode': 'slider', + 'step': 1.0, + 'unit_of_measurement': 'K', + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'default': 6000, + 'name': 'color_temp_max', + 'required': True, + 'selector': dict({ + 'number': dict({ + 'max': 10000.0, + 'min': 1.0, + 'mode': 'slider', + 'step': 1.0, + 'unit_of_measurement': 'K', + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'collapsible': True, + 'name': 'color', + 'optional': True, + 'required': False, + 'schema': list([ + dict({ + 'schema': list([ + dict({ + 'name': 'ga_color', + 'optional': True, + 'options': dict({ + 'dptSelect': list([ + dict({ + 'dpt': dict({ + 'main': 232, + 'sub': 600, + }), + 'translation_key': '232_600', + 'value': '232.600', + }), + dict({ + 'dpt': dict({ + 'main': 251, + 'sub': 600, + }), + 'translation_key': '251_600', + 'value': '251.600', + }), + dict({ + 'dpt': dict({ + 'main': 242, + 'sub': 600, + }), + 'translation_key': '242_600', + 'value': '242.600', + }), + ]), + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'write': dict({ + 'required': True, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + ]), + 'translation_key': 'single_address', + 'type': 'knx_group_select_option', + }), + dict({ + 'schema': list([ + dict({ + 'collapsible': False, + 'name': 'section_red', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_red_switch', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'ga_red_brightness', + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': True, + 'type': 'knx_group_address', + }), + dict({ + 'collapsible': False, + 'name': 'section_green', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_green_switch', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'ga_green_brightness', + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': True, + 'type': 'knx_group_address', + }), + dict({ + 'collapsible': False, + 'name': 'section_blue', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_blue_brightness', + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': True, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'ga_blue_switch', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'collapsible': False, + 'name': 'section_white', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_white_brightness', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'ga_white_switch', + 'optional': True, + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 1, + 'sub': None, + }), + ]), + 'write': dict({ + 'required': False, + }), + }), + 'required': False, + 'type': 'knx_group_address', + }), + ]), + 'translation_key': 'individual_addresses', + 'type': 'knx_group_select_option', + }), + dict({ + 'schema': list([ + dict({ + 'name': 'ga_hue', + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': True, + 'type': 'knx_group_address', + }), + dict({ + 'name': 'ga_saturation', + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'validDPTs': list([ + dict({ + 'main': 5, + 'sub': 1, + }), + ]), + 'write': dict({ + 'required': True, + }), + }), + 'required': True, + 'type': 'knx_group_address', + }), + ]), + 'translation_key': 'hsv_addresses', + 'type': 'knx_group_select_option', + }), + ]), + 'type': 'knx_group_select', + }), + dict({ + 'allow_false': False, + 'default': True, + 'name': 'sync_state', + 'optional': True, + 'required': False, + 'type': 'knx_sync_state', + }), + ]), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_knx_get_schema[switch] + dict({ + 'id': 1, + 'result': list([ + dict({ + 'collapsible': False, + 'name': 'section_switch', + 'required': False, + 'type': 'knx_section_flat', + }), + dict({ + 'name': 'ga_switch', + 'options': dict({ + 'passive': True, + 'state': dict({ + 'required': False, + }), + 'write': dict({ + 'required': True, + }), + }), + 'required': True, + 'type': 'knx_group_address', + }), + dict({ + 'default': False, + 'name': 'invert', + 'optional': True, + 'required': False, + 'selector': dict({ + 'boolean': dict({ + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'default': False, + 'name': 'respond_to_read', + 'optional': True, + 'required': False, + 'selector': dict({ + 'boolean': dict({ + }), + }), + 'type': 'ha_selector', + }), + dict({ + 'allow_false': False, + 'default': True, + 'name': 'sync_state', + 'optional': True, + 'required': False, + 'type': 'knx_sync_state', + }), + ]), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_knx_get_schema[tts] + dict({ + 'error': dict({ + 'code': 'home_assistant_error', + 'message': 'Unknown platform', + }), + 'id': 1, + 'success': False, + 'type': 'result', + }) +# --- diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index e4a208906c6..124ce60e475 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -301,6 +301,7 @@ async def test_get_trigger_capabilities( { "name": "destination", "optional": True, + "required": False, "selector": { "select": { "custom_value": True, @@ -314,6 +315,7 @@ async def test_get_trigger_capabilities( { "name": "group_value_write", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -322,6 +324,7 @@ async def test_get_trigger_capabilities( { "name": "group_value_response", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -330,6 +333,7 @@ async def test_get_trigger_capabilities( { "name": "group_value_read", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -338,6 +342,7 @@ async def test_get_trigger_capabilities( { "name": "incoming", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -346,6 +351,7 @@ async def test_get_trigger_capabilities( { "name": "outgoing", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, diff --git a/tests/components/knx/test_knx_selectors.py b/tests/components/knx/test_knx_selectors.py index 12acf691c08..61bcb71eb5e 100644 --- a/tests/components/knx/test_knx_selectors.py +++ b/tests/components/knx/test_knx_selectors.py @@ -4,9 +4,20 @@ from typing import Any import pytest import voluptuous as vol +from voluptuous_serialize import convert from homeassistant.components.knx.const import ColorTempModes -from homeassistant.components.knx.storage.knx_selector import GASelector +from homeassistant.components.knx.storage.knx_selector import ( + AllSerializeFirst, + GASelector, + GroupSelect, + GroupSelectOption, + KNXSection, + KNXSectionFlat, + SyncStateSelector, +) +from homeassistant.components.knx.storage.serialize import knx_serializer +from homeassistant.helpers import selector INVALID = "invalid" @@ -14,48 +25,6 @@ INVALID = "invalid" @pytest.mark.parametrize( ("selector_config", "data", "expected"), [ - # empty data is invalid - ( - {}, - {}, - {INVALID: "At least one group address must be set"}, - ), - ( - {"write": False}, - {}, - {INVALID: "At least one group address must be set"}, - ), - ( - {"passive": False}, - {}, - {INVALID: "At least one group address must be set"}, - ), - ( - {"write": False, "state": False, "passive": False}, - {}, - {INVALID: "At least one group address must be set"}, - ), - # stale data is invalid - ( - {"write": False}, - {"write": "1/2/3"}, - {INVALID: "At least one group address must be set"}, - ), - ( - {"write": False}, - {"passive": []}, - {INVALID: "At least one group address must be set"}, - ), - ( - {"state": False}, - {"write": None}, - {INVALID: "At least one group address must be set"}, - ), - ( - {"passive": False}, - {"passive": ["1/2/3"]}, - {INVALID: "At least one group address must be set"}, - ), # valid data ( {}, @@ -93,16 +62,6 @@ INVALID = "invalid" {"write": "1/2/3", "state": None}, ), # required keys - ( - {"write_required": True}, - {}, - {INVALID: r"required key not provided*"}, - ), - ( - {"state_required": True}, - {}, - {INVALID: r"required key not provided*"}, - ), ( {"write_required": True}, {"write": "1/2/3"}, @@ -113,32 +72,12 @@ INVALID = "invalid" {"state": "1/2/3"}, {"write": None, "state": "1/2/3", "passive": []}, ), - ( - {"write_required": True}, - {"state": "1/2/3"}, - {INVALID: r"required key not provided*"}, - ), - ( - {"state_required": True}, - {"write": "1/2/3"}, - {INVALID: r"required key not provided*"}, - ), # dpt key - ( - {"dpt": ColorTempModes}, - {"write": "1/2/3"}, - {INVALID: r"required key not provided*"}, - ), ( {"dpt": ColorTempModes}, {"write": "1/2/3", "dpt": "7.600"}, {"write": "1/2/3", "state": None, "passive": [], "dpt": "7.600"}, ), - ( - {"dpt": ColorTempModes}, - {"write": "1/2/3", "state": None, "passive": [], "dpt": "invalid"}, - {INVALID: r"value must be one of ['5.001', '7.600', '9']*"}, - ), ], ) def test_ga_selector( @@ -148,9 +87,267 @@ def test_ga_selector( ) -> None: """Test GASelector.""" selector = GASelector(**selector_config) - if INVALID in expected: - with pytest.raises(vol.Invalid, match=expected[INVALID]): - selector(data) - else: - result = selector(data) - assert result == expected + result = selector(data) + assert result == expected + + +@pytest.mark.parametrize( + ("selector_config", "data", "error_str"), + [ + # empty data is invalid + ( + {}, + {}, + "At least one group address must be set", + ), + ( + {"write": False}, + {}, + "At least one group address must be set", + ), + ( + {"passive": False}, + {}, + "At least one group address must be set", + ), + ( + {"write": False, "state": False, "passive": False}, + {}, + "At least one group address must be set", + ), + # stale data is invalid + ( + {"write": False}, + {"write": "1/2/3"}, + "At least one group address must be set", + ), + ( + {"write": False}, + {"passive": []}, + "At least one group address must be set", + ), + ( + {"state": False}, + {"write": None}, + "At least one group address must be set", + ), + ( + {"passive": False}, + {"passive": ["1/2/3"]}, + "At least one group address must be set", + ), + # required keys + ( + {"write_required": True}, + {}, + r"required key not provided*", + ), + ( + {"state_required": True}, + {}, + r"required key not provided*", + ), + ( + {"write_required": True}, + {"state": "1/2/3"}, + r"required key not provided*", + ), + ( + {"state_required": True}, + {"write": "1/2/3"}, + r"required key not provided*", + ), + # dpt key + ( + {"dpt": ColorTempModes}, + {"write": "1/2/3"}, + r"required key not provided*", + ), + ( + {"dpt": ColorTempModes}, + {"write": "1/2/3", "state": None, "passive": [], "dpt": "invalid"}, + r"value must be one of ['5.001', '7.600', '9']*", + ), + ], +) +def test_ga_selector_invalid( + selector_config: dict[str, Any], + data: dict[str, Any], + error_str: str, +) -> None: + """Test GASelector.""" + selector = GASelector(**selector_config) + with pytest.raises(vol.Invalid, match=error_str): + selector(data) + + +def test_sync_state_selector() -> None: + """Test SyncStateSelector.""" + selector = SyncStateSelector() + assert selector("expire 50") == "expire 50" + + with pytest.raises(vol.Invalid): + selector("invalid") + + with pytest.raises(vol.Invalid, match="Sync state cannot be False"): + selector(False) + + false_allowed = SyncStateSelector(allow_false=True) + assert false_allowed(False) is False + + +@pytest.mark.parametrize( + ("selector", "serialized"), + [ + ( + GASelector(), + { + "type": "knx_group_address", + "options": { + "write": {"required": False}, + "state": {"required": False}, + "passive": True, + }, + }, + ), + ( + GASelector( + state=False, write_required=True, passive=False, valid_dpt="5.001" + ), + { + "type": "knx_group_address", + "options": { + "write": {"required": True}, + "state": False, + "passive": False, + "validDPTs": [{"main": 5, "sub": 1}], + }, + }, + ), + ( + GASelector(dpt=ColorTempModes), + { + "type": "knx_group_address", + "options": { + "write": {"required": False}, + "state": {"required": False}, + "passive": True, + "dptSelect": [ + { + "value": "7.600", + "translation_key": "7_600", + "dpt": {"main": 7, "sub": 600}, + }, + { + "value": "9", + "translation_key": "9", + "dpt": {"main": 9, "sub": None}, + }, + { + "value": "5.001", + "translation_key": "5_001", + "dpt": {"main": 5, "sub": 1}, + }, + ], + }, + }, + ), + ], +) +def test_ga_selector_serialization( + selector: GASelector, serialized: dict[str, Any] +) -> None: + """Test GASelector serialization.""" + assert selector.serialize() == serialized + + +@pytest.mark.parametrize( + ("schema", "serialized"), + [ + ( + AllSerializeFirst(vol.Schema({"key": int}), vol.Schema({"ignored": str})), + [{"name": "key", "required": False, "type": "integer"}], + ), + ( + KNXSectionFlat(collapsible=True), + {"type": "knx_section_flat", "collapsible": True}, + ), + ( + KNXSection( + collapsible=True, + schema={"key": int}, + ), + { + "type": "knx_section", + "collapsible": True, + "schema": [{"name": "key", "required": False, "type": "integer"}], + }, + ), + ( + GroupSelect( + GroupSelectOption(translation_key="option_1", schema={"key_1": str}), + GroupSelectOption(translation_key="option_2", schema={"key_2": int}), + ), + { + "type": "knx_group_select", + "collapsible": True, + "schema": [ + { + "type": "knx_group_select_option", + "translation_key": "option_1", + "schema": [ + {"name": "key_1", "required": False, "type": "string"} + ], + }, + { + "type": "knx_group_select_option", + "translation_key": "option_2", + "schema": [ + {"name": "key_2", "required": False, "type": "integer"} + ], + }, + ], + }, + ), + ( + SyncStateSelector(), + { + "type": "knx_sync_state", + "allow_false": False, + }, + ), + ( + selector.BooleanSelector(), + { + "type": "ha_selector", + "selector": {"boolean": {}}, + }, + ), + ( # in a dict schema `name` and `required` keys are added + vol.Schema( + { + "section_test": KNXSectionFlat(), + vol.Optional("key"): selector.BooleanSelector(), + } + ), + [ + { + "name": "section_test", + "type": "knx_section_flat", + "required": False, + "collapsible": False, + }, + { + "name": "key", + "optional": True, + "required": False, + "type": "ha_selector", + "selector": {"boolean": {}}, + }, + ], + ), + ], +) +def test_serialization(schema: Any, serialized: dict[str, Any]) -> None: + """Test serialization of the selector.""" + assert convert(schema, custom_serializer=knx_serializer) == serialized diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index 5c0f002a541..3f8f9f0da6c 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -4,8 +4,13 @@ from typing import Any from unittest.mock import patch import pytest +from syrupy.assertion import SnapshotAssertion -from homeassistant.components.knx.const import KNX_ADDRESS, KNX_MODULE_KEY +from homeassistant.components.knx.const import ( + KNX_ADDRESS, + KNX_MODULE_KEY, + SUPPORTED_PLATFORMS_UI, +) from homeassistant.components.knx.project import STORAGE_KEY as KNX_PROJECT_STORAGE_KEY from homeassistant.components.knx.schema import SwitchSchema from homeassistant.const import CONF_NAME @@ -16,42 +21,47 @@ from .conftest import KNXTestKit from tests.typing import WebSocketGenerator -async def test_knx_info_command( +async def test_knx_get_base_data_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator ) -> None: - """Test knx/info command.""" + """Test knx/get_base_data command.""" await knx.setup_integration() client = await hass_ws_client(hass) - await client.send_json_auto_id({"type": "knx/info"}) + await client.send_json_auto_id({"type": "knx/get_base_data"}) res = await client.receive_json() assert res["success"], res - assert res["result"]["version"] is not None - assert res["result"]["connected"] - assert res["result"]["current_address"] == "0.0.0" - assert res["result"]["project"] is None + assert res["result"]["connection_info"]["version"] is not None + assert res["result"]["connection_info"]["connected"] + assert res["result"]["connection_info"]["current_address"] == "0.0.0" + assert res["result"]["project_info"] is None + assert not SUPPORTED_PLATFORMS_UI.difference(res["result"]["supported_platforms"]) @pytest.mark.usefixtures("load_knxproj") -async def test_knx_info_command_with_project( +async def test_knx_get_base_data_command_with_project( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator, ) -> None: - """Test knx/info command with loaded project.""" + """Test knx/get_base_data command with loaded project.""" await knx.setup_integration() client = await hass_ws_client(hass) - await client.send_json_auto_id({"type": "knx/info"}) + await client.send_json_auto_id({"type": "knx/get_base_data"}) res = await client.receive_json() assert res["success"], res - assert res["result"]["version"] is not None - assert res["result"]["connected"] - assert res["result"]["current_address"] == "0.0.0" - assert res["result"]["project"] is not None - assert res["result"]["project"]["name"] == "Fixture" - assert res["result"]["project"]["last_modified"] == "2023-04-30T09:04:04.4043671Z" - assert res["result"]["project"]["tool_version"] == "5.7.1428.39779" + + connection_info = res["result"]["connection_info"] + assert connection_info["version"] is not None + assert connection_info["connected"] + assert connection_info["current_address"] == "0.0.0" + + project_info = res["result"]["project_info"] + assert project_info is not None + assert project_info["name"] == "Fixture" + assert project_info["last_modified"] == "2023-04-30T09:04:04.4043671Z" + assert project_info["tool_version"] == "5.7.1428.39779" async def test_knx_project_file_process( @@ -160,8 +170,24 @@ async def test_knx_get_project( await client.send_json_auto_id({"type": "knx/get_knx_project"}) res = await client.receive_json() assert res["success"], res - assert res["result"]["project_loaded"] is True - assert res["result"]["knxproject"] == project_data + assert res["result"] == project_data + + +async def test_knx_get_project_no_project( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, + project_data: dict[str, Any], +) -> None: + """Test retrieval of kxnproject from store.""" + await knx.setup_integration() + client = await hass_ws_client(hass) + assert not hass.data[KNX_MODULE_KEY].project.loaded + + await client.send_json_auto_id({"type": "knx/get_knx_project"}) + res = await client.receive_json() + assert res["success"], res + assert res["result"] is None async def test_knx_group_monitor_info_command( @@ -390,10 +416,26 @@ async def test_knx_subscribe_telegrams_command_project( assert res["event"]["timestamp"] is not None +@pytest.mark.parametrize("platform", sorted({*SUPPORTED_PLATFORMS_UI, "tts"})) +async def test_knx_get_schema( + hass: HomeAssistant, + knx: KNXTestKit, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, + platform: str, +) -> None: + """Test knx/get_schema command returning proper schema data.""" + await knx.setup_integration() + client = await hass_ws_client(hass) + await client.send_json_auto_id({"type": "knx/get_schema", "platform": platform}) + res = await client.receive_json() + assert res == snapshot + + @pytest.mark.parametrize( "endpoint", [ - "knx/info", # sync ws-command + "knx/get_base_data", # sync ws-command "knx/get_knx_project", # async ws-command ], ) diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 94eb96591e2..9f134a0c203 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -349,7 +349,15 @@ async def test_get_transponder_trigger_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] + ) == [ + { + "name": "code", + "optional": True, + "required": False, + "type": "string", + "lower": True, + } + ] async def test_get_fingerprint_trigger_capabilities( @@ -373,7 +381,15 @@ async def test_get_fingerprint_trigger_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] + ) == [ + { + "name": "code", + "optional": True, + "required": False, + "type": "string", + "lower": True, + } + ] async def test_get_transmitter_trigger_capabilities( @@ -398,13 +414,32 @@ async def test_get_transmitter_trigger_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ - {"name": "code", "type": "string", "optional": True, "lower": True}, - {"name": "level", "type": "integer", "optional": True, "valueMin": 0}, - {"name": "key", "type": "integer", "optional": True, "valueMin": 0}, + { + "name": "code", + "type": "string", + "optional": True, + "required": False, + "lower": True, + }, + { + "name": "level", + "type": "integer", + "optional": True, + "required": False, + "valueMin": 0, + }, + { + "name": "key", + "type": "integer", + "optional": True, + "required": False, + "valueMin": 0, + }, { "name": "action", "type": "select", "optional": True, + "required": False, "options": [("hit", "hit"), ("make", "make"), ("break", "break")], }, ] @@ -436,6 +471,7 @@ async def test_get_send_keys_trigger_capabilities( "name": "key", "type": "select", "optional": True, + "required": False, "options": [(send_key.lower(), send_key.lower()) for send_key in SENDKEYS], }, { @@ -445,6 +481,7 @@ async def test_get_send_keys_trigger_capabilities( (key_action.lower(), key_action.lower()) for key_action in KEY_ACTIONS ], "optional": True, + "required": False, }, ] diff --git a/tests/components/letpot/__init__.py b/tests/components/letpot/__init__.py index d8be422899a..644b8e1580f 100644 --- a/tests/components/letpot/__init__.py +++ b/tests/components/letpot/__init__.py @@ -33,7 +33,7 @@ AUTHENTICATION = AuthenticationInfo( MAX_STATUS = LetPotDeviceStatus( errors=LetPotDeviceErrors(low_water=True, low_nutrients=False, refill_error=False), - light_brightness=500, + light_brightness=750, light_mode=LightMode.VEGETABLE, light_schedule_end=datetime.time(18, 0), light_schedule_start=datetime.time(8, 0), diff --git a/tests/components/letpot/conftest.py b/tests/components/letpot/conftest.py index 03ce2ec4a0d..4abf917cb9f 100644 --- a/tests/components/letpot/conftest.py +++ b/tests/components/letpot/conftest.py @@ -49,6 +49,16 @@ def _mock_device_features(device_type: str) -> DeviceFeature: | DeviceFeature.LIGHT_BRIGHTNESS_LOW_HIGH | DeviceFeature.PUMP_STATUS ) + if device_type == "LPH62": + return ( + DeviceFeature.CATEGORY_HYDROPONIC_GARDEN + | DeviceFeature.LIGHT_BRIGHTNESS_LEVELS + | DeviceFeature.NUTRIENT_BUTTON + | DeviceFeature.PUMP_AUTO + | DeviceFeature.TEMPERATURE + | DeviceFeature.TEMPERATURE_SET_UNIT + | DeviceFeature.WATER_LEVEL + ) if device_type == "LPH63": return ( DeviceFeature.CATEGORY_HYDROPONIC_GARDEN @@ -66,11 +76,20 @@ def _mock_device_status(device_type: str) -> LetPotDeviceStatus: """Return mock device status for the given type.""" if device_type == "LPH31": return SE_STATUS - if device_type == "LPH63": + if device_type in {"LPH62", "LPH63"}: return MAX_STATUS raise ValueError(f"No mock data for device type {device_type}") +def _mock_light_brightness_levels(device_type: str) -> list[int]: + """Return mock brightness levels for the given type.""" + if device_type == "LPH31": + return [500, 1000] + if device_type in {"LPH62", "LPH63"}: + return [125, 250, 375, 500, 625, 750, 875, 1000] + raise ValueError(f"No mock data for device type {device_type}") + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" @@ -134,6 +153,9 @@ def mock_device_client() -> Generator[AsyncMock]: device_client.device_info.side_effect = lambda serial: _mock_device_info( serial[:5] ) + device_client.get_light_brightness_levels.side_effect = ( + lambda serial: _mock_light_brightness_levels(serial[:5]) + ) device_client.get_current_status.side_effect = get_current_status_side_effect device_client.request_status_update.side_effect = request_status_side_effect device_client.subscribe.side_effect = subscribe_side_effect diff --git a/tests/components/linear_garage_door/snapshots/test_light.ambr b/tests/components/letpot/snapshots/test_select.ambr similarity index 52% rename from tests/components/linear_garage_door/snapshots/test_light.ambr rename to tests/components/letpot/snapshots/test_select.ambr index 930d78d4706..5d9ddf0d0d3 100644 --- a/tests/components/linear_garage_door/snapshots/test_light.ambr +++ b/tests/components/letpot/snapshots/test_select.ambr @@ -1,12 +1,13 @@ # serializer version: 1 -# name: test_data[light.test_garage_1_light-entry] +# name: test_all_entities[LPH31][select.garden_light_brightness-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'supported_color_modes': list([ - , + 'options': list([ + 'low', + 'high', ]), }), 'config_entry_id': , @@ -14,9 +15,9 @@ 'device_class': None, 'device_id': , 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_1_light', + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_light_brightness', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -28,43 +29,42 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', + 'original_name': 'Light brightness', + 'platform': 'letpot', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test1-Light', + 'translation_key': 'light_brightness', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_light_brightness_low_high', 'unit_of_measurement': None, }) # --- -# name: test_data[light.test_garage_1_light-state] +# name: test_all_entities[LPH31][select.garden_light_brightness-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'friendly_name': 'Test Garage 1 Light', - 'supported_color_modes': list([ - , + 'friendly_name': 'Garden Light brightness', + 'options': list([ + 'low', + 'high', ]), - 'supported_features': , }), 'context': , - 'entity_id': 'light.test_garage_1_light', + 'entity_id': 'select.garden_light_brightness', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'low', }) # --- -# name: test_data[light.test_garage_2_light-entry] +# name: test_all_entities[LPH31][select.garden_light_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'supported_color_modes': list([ - , + 'options': list([ + 'flower', + 'vegetable', ]), }), 'config_entry_id': , @@ -72,9 +72,9 @@ 'device_class': None, 'device_id': , 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_2_light', + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_light_mode', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -86,43 +86,42 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', + 'original_name': 'Light mode', + 'platform': 'letpot', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test2-Light', + 'translation_key': 'light_mode', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_light_mode', 'unit_of_measurement': None, }) # --- -# name: test_data[light.test_garage_2_light-state] +# name: test_all_entities[LPH31][select.garden_light_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Test Garage 2 Light', - 'supported_color_modes': list([ - , + 'friendly_name': 'Garden Light mode', + 'options': list([ + 'flower', + 'vegetable', ]), - 'supported_features': , }), 'context': , - 'entity_id': 'light.test_garage_2_light', + 'entity_id': 'select.garden_light_mode', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'vegetable', }) # --- -# name: test_data[light.test_garage_3_light-entry] +# name: test_all_entities[LPH62][select.garden_light_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'supported_color_modes': list([ - , + 'options': list([ + 'flower', + 'vegetable', ]), }), 'config_entry_id': , @@ -130,9 +129,9 @@ 'device_class': None, 'device_id': , 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_3_light', + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_light_mode', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -144,43 +143,42 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', + 'original_name': 'Light mode', + 'platform': 'letpot', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test3-Light', + 'translation_key': 'light_mode', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH62ABCD_light_mode', 'unit_of_measurement': None, }) # --- -# name: test_data[light.test_garage_3_light-state] +# name: test_all_entities[LPH62][select.garden_light_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Test Garage 3 Light', - 'supported_color_modes': list([ - , + 'friendly_name': 'Garden Light mode', + 'options': list([ + 'flower', + 'vegetable', ]), - 'supported_features': , }), 'context': , - 'entity_id': 'light.test_garage_3_light', + 'entity_id': 'select.garden_light_mode', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'vegetable', }) # --- -# name: test_data[light.test_garage_4_light-entry] +# name: test_all_entities[LPH62][select.garden_temperature_unit_on_display-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'supported_color_modes': list([ - , + 'options': list([ + 'fahrenheit', + 'celsius', ]), }), 'config_entry_id': , @@ -188,9 +186,9 @@ 'device_class': None, 'device_id': , 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.test_garage_4_light', + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_temperature_unit_on_display', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -202,32 +200,30 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Light', - 'platform': 'linear_garage_door', + 'original_name': 'Temperature unit on display', + 'platform': 'letpot', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'light', - 'unique_id': 'test4-Light', + 'translation_key': 'display_temperature_unit', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH62ABCD_display_temperature_unit', 'unit_of_measurement': None, }) # --- -# name: test_data[light.test_garage_4_light-state] +# name: test_all_entities[LPH62][select.garden_temperature_unit_on_display-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'friendly_name': 'Test Garage 4 Light', - 'supported_color_modes': list([ - , + 'friendly_name': 'Garden Temperature unit on display', + 'options': list([ + 'fahrenheit', + 'celsius', ]), - 'supported_features': , }), 'context': , - 'entity_id': 'light.test_garage_4_light', + 'entity_id': 'select.garden_temperature_unit_on_display', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'celsius', }) # --- diff --git a/tests/components/letpot/test_select.py b/tests/components/letpot/test_select.py new file mode 100644 index 00000000000..d576ca6fca6 --- /dev/null +++ b/tests/components/letpot/test_select.py @@ -0,0 +1,102 @@ +"""Test select entities for the LetPot integration.""" + +from unittest.mock import MagicMock, patch + +from letpot.exceptions import LetPotConnectionException, LetPotException +from letpot.models import LightMode +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize("device_type", ["LPH62", "LPH31"]) +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_type: str, +) -> None: + """Test switch entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("device_type", ["LPH31"]) +async def test_set_select( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + device_type: str, +) -> None: + """Test select entity set to value.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.garden_light_brightness", + ATTR_OPTION: "high", + }, + blocking=True, + ) + + mock_device_client.set_light_brightness.assert_awaited_once_with( + f"{device_type}ABCD", 1000 + ) + + +@pytest.mark.parametrize( + ("exception", "user_error"), + [ + ( + LetPotConnectionException("Connection failed"), + "An error occurred while communicating with the LetPot device: Connection failed", + ), + ( + LetPotException("Random thing failed"), + "An unknown error occurred while communicating with the LetPot device: Random thing failed", + ), + ], +) +async def test_select_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + exception: Exception, + user_error: str, +) -> None: + """Test select entity exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_device_client.set_light_mode.side_effect = exception + + with pytest.raises(HomeAssistantError, match=user_error): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.garden_light_mode", + ATTR_OPTION: LightMode.FLOWER.name.lower(), + }, + blocking=True, + ) diff --git a/tests/components/lg_thinq/conftest.py b/tests/components/lg_thinq/conftest.py index 2eaddf1a83b..73abc8c5075 100644 --- a/tests/components/lg_thinq/conftest.py +++ b/tests/components/lg_thinq/conftest.py @@ -98,15 +98,6 @@ def mock_thinq_api(mock_thinq_mqtt_client: None) -> Generator[AsyncMock]: """Mock a thinq api.""" with patch("homeassistant.components.lg_thinq.ThinQApi", autospec=True) as mock_api: thinq_api = mock_api.return_value - thinq_api.async_get_device_list.return_value = [ - load_json_object_fixture("air_conditioner/device.json", DOMAIN) - ] - thinq_api.async_get_device_profile.return_value = load_json_object_fixture( - "air_conditioner/profile.json", DOMAIN - ) - thinq_api.async_get_device_status.return_value = load_json_object_fixture( - "air_conditioner/status.json", DOMAIN - ) yield thinq_api @@ -119,3 +110,31 @@ def mock_thinq_mqtt_client() -> Generator[None]: return_value=True, ): yield + + +@pytest.fixture( + params=[ + "air_conditioner", + "washer", + ] +) +def device_fixture( + mock_thinq_api: AsyncMock, request: pytest.FixtureRequest +) -> Generator[str]: + """Return every device.""" + return request.param + + +@pytest.fixture +def devices(mock_thinq_api: AsyncMock, device_fixture: str) -> Generator[AsyncMock]: + """Return a specific device.""" + mock_thinq_api.async_get_device_list.return_value = [ + load_json_object_fixture(f"{device_fixture}/device.json", DOMAIN) + ] + mock_thinq_api.async_get_device_profile.return_value = load_json_object_fixture( + f"{device_fixture}/profile.json", DOMAIN + ) + mock_thinq_api.async_get_device_status.return_value = load_json_object_fixture( + f"{device_fixture}/status.json", DOMAIN + ) + return mock_thinq_api diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/status.json b/tests/components/lg_thinq/fixtures/air_conditioner/status.json index 8440e7da28c..bffb13a1ac1 100644 --- a/tests/components/lg_thinq/fixtures/air_conditioner/status.json +++ b/tests/components/lg_thinq/fixtures/air_conditioner/status.json @@ -44,7 +44,6 @@ "unit": "F" } ], - "timer": { "relativeStartTimer": "UNSET", "relativeStopTimer": "UNSET", diff --git a/tests/components/lg_thinq/fixtures/washer/device.json b/tests/components/lg_thinq/fixtures/washer/device.json new file mode 100644 index 00000000000..33ea13669bd --- /dev/null +++ b/tests/components/lg_thinq/fixtures/washer/device.json @@ -0,0 +1,9 @@ +{ + "deviceId": "MW2-0B530EFD-1ADF-4F54-A2C3-46C37F94C689", + "deviceInfo": { + "deviceType": "DEVICE_WASHER", + "modelName": "FAFXU22027", + "alias": "Test washer", + "reportable": true + } +} diff --git a/tests/components/lg_thinq/fixtures/washer/profile.json b/tests/components/lg_thinq/fixtures/washer/profile.json new file mode 100644 index 00000000000..6b78300b4f9 --- /dev/null +++ b/tests/components/lg_thinq/fixtures/washer/profile.json @@ -0,0 +1,151 @@ +{ + "notification": { + "push": ["WASHING_IS_COMPLETE", "ERROR_DURING_WASHING"] + }, + "property": [ + { + "cycle": { + "cycleCount": { + "mode": ["r"], + "type": "number" + } + }, + "detergent": { + "detergentSetting": "NORMAL" + }, + "location": { + "locationName": "MAIN" + }, + "operation": { + "washerOperationMode": { + "mode": ["w"], + "type": "enum", + "value": { + "w": ["START", "STOP", "POWER_OFF", "POWER_ON"] + } + } + }, + "remoteControlEnable": { + "remoteControlEnabled": { + "mode": ["r"], + "type": "boolean", + "value": { + "r": [false, true] + } + } + }, + "runState": { + "currentState": { + "mode": ["r"], + "type": "enum", + "value": { + "r": [ + "DISPENSING", + "END", + "RUNNING", + "FROZEN_PREVENT_RUNNING", + "PAUSE", + "PREWASH", + "DETECTING", + "INITIAL", + "SOAKING", + "DRYING", + "FROZEN_PREVENT_PAUSE", + "FROZEN_PREVENT_INITIAL", + "REFRESHING", + "RINSING", + "STEAM_SOFTENING", + "DETERGENT_AMOUNT", + "POWER_OFF", + "RESERVED", + "RINSE_HOLD", + "ERROR", + "SPINNING", + "ADD_DRAIN" + ] + } + } + }, + "timer": { + "relativeHourToStart": { + "mode": ["r", "w"], + "type": "range", + "value": { + "r": { + "except": [], + "max": 19, + "min": 1, + "step": 1 + }, + "w": { + "except": [], + "max": 19, + "min": 1, + "step": 1 + } + } + }, + "relativeMinuteToStart": { + "mode": ["r"], + "type": "range", + "value": { + "r": { + "except": [], + "max": 30, + "min": 0, + "step": 1 + } + } + }, + "remainHour": { + "mode": ["r"], + "type": "range", + "value": { + "r": { + "except": [], + "max": 30, + "min": 0, + "step": 1 + } + } + }, + "remainMinute": { + "mode": ["r"], + "type": "range", + "value": { + "r": { + "except": [], + "max": 59, + "min": 0, + "step": 1 + } + } + }, + "totalHour": { + "mode": ["r"], + "type": "range", + "value": { + "r": { + "except": [], + "max": 30, + "min": 0, + "step": 1 + } + } + }, + "totalMinute": { + "mode": ["r"], + "type": "range", + "value": { + "r": { + "except": [], + "max": 59, + "min": 0, + "step": 1 + } + } + } + } + } + ] +} diff --git a/tests/components/lg_thinq/fixtures/washer/status.json b/tests/components/lg_thinq/fixtures/washer/status.json new file mode 100644 index 00000000000..f325bc34691 --- /dev/null +++ b/tests/components/lg_thinq/fixtures/washer/status.json @@ -0,0 +1,22 @@ +{ + "cycle": { + "cycleCount": 10 + }, + "location": { + "locationName": "MAIN" + }, + "remoteControlEnable": { + "remoteControlEnabled": false + }, + "runState": { + "currentState": "POWER_OFF" + }, + "timer": { + "relativeHourToStart": 0, + "relativeMinuteToStart": 0, + "remainHour": 0, + "remainMinute": 45, + "totalHour": 0, + "totalMinute": 50 + } +} diff --git a/tests/components/lg_thinq/snapshots/test_climate.ambr b/tests/components/lg_thinq/snapshots/test_climate.ambr index 754969ff549..5c05244b313 100644 --- a/tests/components/lg_thinq/snapshots/test_climate.ambr +++ b/tests/components/lg_thinq/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[climate.test_air_conditioner-entry] +# name: test_climate_entities[air_conditioner][climate.test_air_conditioner-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -8,7 +8,7 @@ 'fan_modes': list([ 'low', 'high', - 'mid', + 'medium', ]), 'hvac_modes': list([ , @@ -60,16 +60,16 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[climate.test_air_conditioner-state] +# name: test_climate_entities[air_conditioner][climate.test_air_conditioner-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_humidity': 40, 'current_temperature': 77, - 'fan_mode': 'mid', + 'fan_mode': 'medium', 'fan_modes': list([ 'low', 'high', - 'mid', + 'medium', ]), 'friendly_name': 'Test air conditioner', 'hvac_modes': list([ diff --git a/tests/components/lg_thinq/snapshots/test_event.ambr b/tests/components/lg_thinq/snapshots/test_event.ambr index 670ce8985fa..3f9e11849ab 100644 --- a/tests/components/lg_thinq/snapshots/test_event.ambr +++ b/tests/components/lg_thinq/snapshots/test_event.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[event.test_air_conditioner_notification-entry] +# name: test_event_entities[air_conditioner][event.test_air_conditioner_notification-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -38,7 +38,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[event.test_air_conditioner_notification-state] +# name: test_event_entities[air_conditioner][event.test_air_conditioner_notification-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'event_type': None, diff --git a/tests/components/lg_thinq/snapshots/test_number.ambr b/tests/components/lg_thinq/snapshots/test_number.ambr index 5fa03b60033..28403ab7337 100644 --- a/tests/components/lg_thinq/snapshots/test_number.ambr +++ b/tests/components/lg_thinq/snapshots/test_number.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-entry] +# name: test_number_entities[air_conditioner][number.test_air_conditioner_schedule_turn_off-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -39,7 +39,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-state] +# name: test_number_entities[air_conditioner][number.test_air_conditioner_schedule_turn_off-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test air conditioner Schedule turn-off', @@ -57,7 +57,7 @@ 'state': 'unknown', }) # --- -# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-entry] +# name: test_number_entities[air_conditioner][number.test_air_conditioner_schedule_turn_on-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -97,7 +97,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-state] +# name: test_number_entities[air_conditioner][number.test_air_conditioner_schedule_turn_on-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test air conditioner Schedule turn-on', @@ -115,3 +115,61 @@ 'state': 'unknown', }) # --- +# name: test_number_entities[washer][number.test_washer_delayed_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 19, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.test_washer_delayed_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Delayed start', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-0B530EFD-1ADF-4F54-A2C3-46C37F94C689_main_relative_hour_to_start', + 'unit_of_measurement': , + }) +# --- +# name: test_number_entities[washer][number.test_washer_delayed_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test washer Delayed start', + 'max': 19, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.test_washer_delayed_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/lg_thinq/snapshots/test_sensor.ambr b/tests/components/lg_thinq/snapshots/test_sensor.ambr index d561c4c6fc9..1ab4ede5a5b 100644 --- a/tests/components/lg_thinq/snapshots/test_sensor.ambr +++ b/tests/components/lg_thinq/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[sensor.test_air_conditioner_filter_remaining-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_filter_remaining-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -34,7 +34,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.test_air_conditioner_filter_remaining-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_filter_remaining-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test air conditioner Filter remaining', @@ -48,7 +48,7 @@ 'state': '540', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_humidity-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -85,7 +85,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_humidity-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -101,7 +101,7 @@ 'state': '40', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_pm1-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_pm1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -135,16 +135,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm1', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_pm1-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_pm1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm1', 'friendly_name': 'Test air conditioner PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_air_conditioner_pm1', @@ -154,7 +154,7 @@ 'state': '12', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_pm10-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_pm10-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -188,16 +188,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm10', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_pm10-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_pm10-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm10', 'friendly_name': 'Test air conditioner PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_air_conditioner_pm10', @@ -207,7 +207,7 @@ 'state': '7', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_pm2_5-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_pm2_5-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -241,16 +241,16 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_pm2_5-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_pm2_5-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pm25', 'friendly_name': 'Test air conditioner PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_air_conditioner_pm2_5', @@ -260,7 +260,7 @@ 'state': '24', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_off-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_schedule_turn_off-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -298,7 +298,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_off-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_schedule_turn_off-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -313,7 +313,7 @@ 'state': 'unknown', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_schedule_turn_on-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -351,7 +351,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_schedule_turn_on-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -366,7 +366,7 @@ 'state': 'unknown', }) # --- -# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on_2-entry] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_schedule_turn_on_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -401,7 +401,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on_2-state] +# name: test_sensor_entities[air_conditioner][sensor.test_air_conditioner_schedule_turn_on_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'timestamp', diff --git a/tests/components/lg_thinq/snapshots/test_switch.ambr b/tests/components/lg_thinq/snapshots/test_switch.ambr new file mode 100644 index 00000000000..e427916630b --- /dev/null +++ b/tests/components/lg_thinq/snapshots/test_switch.ambr @@ -0,0 +1,197 @@ +# serializer version: 1 +# name: test_switch_entities[washer][switch.test_washer_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.test_washer_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'operation_power', + 'unique_id': 'MW2-0B530EFD-1ADF-4F54-A2C3-46C37F94C689_main_washer_operation_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[washer][switch.test_washer_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test washer Power', + }), + 'context': , + 'entity_id': 'switch.test_washer_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_entities[air_conditioner][switch.test_air_conditioner_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_air_conditioner_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'operation_power', + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_air_con_operation_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[air_conditioner][switch.test_air_conditioner_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test air conditioner Power', + }), + 'context': , + 'entity_id': 'switch.test_air_conditioner_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_entities[air_conditioner][switch.test_air_conditioner_energy_saving-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_air_conditioner_energy_saving', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy saving', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_power_save_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[air_conditioner][switch.test_air_conditioner_energy_saving-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test air conditioner Energy saving', + }), + 'context': , + 'entity_id': 'switch.test_air_conditioner_energy_saving', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_entities[air_conditioner][switch.test_air_conditioner_air_purify-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_air_conditioner_air_purify', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air purify', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_air_clean_operation_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[air_conditioner][switch.test_air_conditioner_air_purify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test air conditioner Air purify', + }), + 'context': , + 'entity_id': 'switch.test_air_conditioner_air_purify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- \ No newline at end of file diff --git a/tests/components/lg_thinq/test_climate.py b/tests/components/lg_thinq/test_climate.py index c79331dd638..6b80151805c 100644 --- a/tests/components/lg_thinq/test_climate.py +++ b/tests/components/lg_thinq/test_climate.py @@ -16,9 +16,11 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_all_entities( +@pytest.mark.parametrize("device_fixture", ["air_conditioner"]) +async def test_climate_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, + devices: AsyncMock, mock_thinq_api: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, diff --git a/tests/components/lg_thinq/test_config_flow.py b/tests/components/lg_thinq/test_config_flow.py index a46162723f0..2612519824d 100644 --- a/tests/components/lg_thinq/test_config_flow.py +++ b/tests/components/lg_thinq/test_config_flow.py @@ -49,8 +49,7 @@ async def test_config_flow( async def test_config_flow_invalid_pat( - hass: HomeAssistant, - mock_invalid_thinq_api: AsyncMock, + hass: HomeAssistant, mock_invalid_thinq_api: AsyncMock ) -> None: """Test that an thinq flow should be aborted with an invalid PAT.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/lg_thinq/test_event.py b/tests/components/lg_thinq/test_event.py index 398af1e8aad..c6aa176c429 100644 --- a/tests/components/lg_thinq/test_event.py +++ b/tests/components/lg_thinq/test_event.py @@ -15,9 +15,11 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_all_entities( +@pytest.mark.parametrize("device_fixture", ["air_conditioner"]) +async def test_event_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, + devices: AsyncMock, mock_thinq_api: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, diff --git a/tests/components/lg_thinq/test_number.py b/tests/components/lg_thinq/test_number.py index 7c37ba3f5e0..b36685a8aa4 100644 --- a/tests/components/lg_thinq/test_number.py +++ b/tests/components/lg_thinq/test_number.py @@ -15,9 +15,10 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_all_entities( +async def test_number_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, + devices: AsyncMock, mock_thinq_api: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, diff --git a/tests/components/lg_thinq/test_sensor.py b/tests/components/lg_thinq/test_sensor.py index e2c8e122eea..87f03de6c0d 100644 --- a/tests/components/lg_thinq/test_sensor.py +++ b/tests/components/lg_thinq/test_sensor.py @@ -15,11 +15,13 @@ from . import setup_integration from tests.common import MockConfigEntry, snapshot_platform +@pytest.mark.parametrize("device_fixture", ["air_conditioner"]) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.freeze_time(datetime(2024, 10, 10, tzinfo=UTC)) -async def test_all_entities( +async def test_sensor_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, + devices: AsyncMock, mock_thinq_api: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, diff --git a/tests/components/lg_thinq/test_switch.py b/tests/components/lg_thinq/test_switch.py new file mode 100644 index 00000000000..0e2039f49d0 --- /dev/null +++ b/tests/components/lg_thinq/test_switch.py @@ -0,0 +1,28 @@ +"""Tests for the LG ThinQ switch platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_switch_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + devices: AsyncMock, + mock_thinq_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index c2ac7087cf0..1f69b586c9b 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -194,6 +194,7 @@ async def test_get_action_capabilities( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -219,6 +220,7 @@ async def test_get_action_capabilities( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -238,6 +240,7 @@ async def test_get_action_capabilities( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } @@ -256,6 +259,7 @@ async def test_get_action_capabilities( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } @@ -341,6 +345,7 @@ async def test_get_action_capabilities_features( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -366,6 +371,7 @@ async def test_get_action_capabilities_features( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -385,6 +391,7 @@ async def test_get_action_capabilities_features( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } @@ -403,6 +410,7 @@ async def test_get_action_capabilities_features( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 2a5c9f0bb18..1dabe6e6071 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -128,7 +128,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -158,7 +163,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index ae54bbd2512..99e0a5e5b93 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -133,7 +133,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -163,7 +168,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/linear_garage_door/__init__.py b/tests/components/linear_garage_door/__init__.py deleted file mode 100644 index 67bd1ee2da2..00000000000 --- a/tests/components/linear_garage_door/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tests for the Linear Garage Door integration.""" - -from unittest.mock import patch - -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_integration( - hass: HomeAssistant, config_entry: MockConfigEntry, platforms: list[Platform] -) -> None: - """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.linear_garage_door.PLATFORMS", - platforms, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() diff --git a/tests/components/linear_garage_door/conftest.py b/tests/components/linear_garage_door/conftest.py deleted file mode 100644 index 4ed7662e5d0..00000000000 --- a/tests/components/linear_garage_door/conftest.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Common fixtures for the Linear Garage Door tests.""" - -from collections.abc import Generator -from unittest.mock import AsyncMock, patch - -import pytest - -from homeassistant.components.linear_garage_door import DOMAIN -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD - -from tests.common import ( - MockConfigEntry, - load_json_array_fixture, - load_json_object_fixture, -) - - -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Override async_setup_entry.""" - with patch( - "homeassistant.components.linear_garage_door.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - yield mock_setup_entry - - -@pytest.fixture -def mock_linear() -> Generator[AsyncMock]: - """Mock a Linear Garage Door client.""" - with ( - patch( - "homeassistant.components.linear_garage_door.coordinator.Linear", - autospec=True, - ) as mock_client, - patch( - "homeassistant.components.linear_garage_door.config_flow.Linear", - new=mock_client, - ), - ): - client = mock_client.return_value - client.login.return_value = True - client.get_devices.return_value = load_json_array_fixture( - "get_devices.json", DOMAIN - ) - client.get_sites.return_value = load_json_array_fixture( - "get_sites.json", DOMAIN - ) - device_states = load_json_object_fixture("get_device_state.json", DOMAIN) - client.get_device_state.side_effect = lambda device_id: device_states[device_id] - yield client - - -@pytest.fixture -def mock_config_entry() -> MockConfigEntry: - """Mock a config entry.""" - return MockConfigEntry( - domain=DOMAIN, - entry_id="acefdd4b3a4a0911067d1cf51414201e", - title="test-site-name", - data={ - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) diff --git a/tests/components/linear_garage_door/fixtures/get_device_state.json b/tests/components/linear_garage_door/fixtures/get_device_state.json deleted file mode 100644 index 14247610e06..00000000000 --- a/tests/components/linear_garage_door/fixtures/get_device_state.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "test1": { - "GDO": { - "Open_B": "true", - "Open_P": "100" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - }, - "test2": { - "GDO": { - "Open_B": "false", - "Open_P": "0" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test3": { - "GDO": { - "Open_B": "false", - "Opening_P": "0" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test4": { - "GDO": { - "Open_B": "true", - "Opening_P": "100" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - } -} diff --git a/tests/components/linear_garage_door/fixtures/get_device_state_1.json b/tests/components/linear_garage_door/fixtures/get_device_state_1.json deleted file mode 100644 index 1f41d4fd153..00000000000 --- a/tests/components/linear_garage_door/fixtures/get_device_state_1.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "test1": { - "GDO": { - "Open_B": "true", - "Opening_P": "100" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test2": { - "GDO": { - "Open_B": "false", - "Opening_P": "0" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - }, - "test3": { - "GDO": { - "Open_B": "false", - "Opening_P": "0" - }, - "Light": { - "On_B": "false", - "On_P": "0" - } - }, - "test4": { - "GDO": { - "Open_B": "true", - "Opening_P": "100" - }, - "Light": { - "On_B": "true", - "On_P": "100" - } - } -} diff --git a/tests/components/linear_garage_door/fixtures/get_devices.json b/tests/components/linear_garage_door/fixtures/get_devices.json deleted file mode 100644 index da6eeaf7448..00000000000 --- a/tests/components/linear_garage_door/fixtures/get_devices.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "id": "test1", - "name": "Test Garage 1", - "subdevices": ["GDO", "Light"] - }, - { - "id": "test2", - "name": "Test Garage 2", - "subdevices": ["GDO", "Light"] - }, - { - "id": "test3", - "name": "Test Garage 3", - "subdevices": ["GDO", "Light"] - }, - { - "id": "test4", - "name": "Test Garage 4", - "subdevices": ["GDO", "Light"] - } -] diff --git a/tests/components/linear_garage_door/fixtures/get_sites.json b/tests/components/linear_garage_door/fixtures/get_sites.json deleted file mode 100644 index 2b0a49b9007..00000000000 --- a/tests/components/linear_garage_door/fixtures/get_sites.json +++ /dev/null @@ -1 +0,0 @@ -[{ "id": "test-site-id", "name": "test-site-name" }] diff --git a/tests/components/linear_garage_door/snapshots/test_cover.ambr b/tests/components/linear_garage_door/snapshots/test_cover.ambr deleted file mode 100644 index dc3df6684bc..00000000000 --- a/tests/components/linear_garage_door/snapshots/test_cover.ambr +++ /dev/null @@ -1,201 +0,0 @@ -# serializer version: 1 -# name: test_covers[cover.test_garage_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test1-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 1', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'open', - }) -# --- -# name: test_covers[cover.test_garage_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test2-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 2', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'closed', - }) -# --- -# name: test_covers[cover.test_garage_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test3-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 3', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'opening', - }) -# --- -# name: test_covers[cover.test_garage_4-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_garage_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'linear_garage_door', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'test4-GDO', - 'unit_of_measurement': None, - }) -# --- -# name: test_covers[cover.test_garage_4-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'garage', - 'friendly_name': 'Test Garage 4', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_garage_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'closing', - }) -# --- diff --git a/tests/components/linear_garage_door/snapshots/test_diagnostics.ambr b/tests/components/linear_garage_door/snapshots/test_diagnostics.ambr deleted file mode 100644 index db82f41eb73..00000000000 --- a/tests/components/linear_garage_door/snapshots/test_diagnostics.ambr +++ /dev/null @@ -1,83 +0,0 @@ -# serializer version: 1 -# name: test_entry_diagnostics - dict({ - 'coordinator_data': dict({ - 'test1': dict({ - 'name': 'Test Garage 1', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'true', - 'Open_P': '100', - }), - 'Light': dict({ - 'On_B': 'true', - 'On_P': '100', - }), - }), - }), - 'test2': dict({ - 'name': 'Test Garage 2', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'false', - 'Open_P': '0', - }), - 'Light': dict({ - 'On_B': 'false', - 'On_P': '0', - }), - }), - }), - 'test3': dict({ - 'name': 'Test Garage 3', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'false', - 'Opening_P': '0', - }), - 'Light': dict({ - 'On_B': 'false', - 'On_P': '0', - }), - }), - }), - 'test4': dict({ - 'name': 'Test Garage 4', - 'subdevices': dict({ - 'GDO': dict({ - 'Open_B': 'true', - 'Opening_P': '100', - }), - 'Light': dict({ - 'On_B': 'true', - 'On_P': '100', - }), - }), - }), - }), - 'entry': dict({ - 'data': dict({ - 'device_id': 'test-uuid', - 'email': '**REDACTED**', - 'password': '**REDACTED**', - 'site_id': 'test-site-id', - }), - 'disabled_by': None, - 'discovery_keys': dict({ - }), - 'domain': 'linear_garage_door', - 'entry_id': 'acefdd4b3a4a0911067d1cf51414201e', - 'minor_version': 1, - 'options': dict({ - }), - 'pref_disable_new_entities': False, - 'pref_disable_polling': False, - 'source': 'user', - 'subentries': list([ - ]), - 'title': 'test-site-name', - 'unique_id': None, - 'version': 1, - }), - }) -# --- diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py deleted file mode 100644 index 64bdc589194..00000000000 --- a/tests/components/linear_garage_door/test_config_flow.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Test the Linear Garage Door config flow.""" - -from unittest.mock import AsyncMock, patch - -from linear_garage_door.errors import InvalidLoginError -import pytest - -from homeassistant.components.linear_garage_door.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType - -from tests.common import MockConfigEntry - - -async def test_form( - hass: HomeAssistant, mock_linear: AsyncMock, mock_setup_entry: AsyncMock -) -> None: - """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert not result["errors"] - - with patch( - "uuid.uuid4", - return_value="test-uuid", - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"site": "test-site-id"} - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-site-name" - assert result["data"] == { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_reauth( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_setup_entry: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test reauthentication.""" - mock_config_entry.add_to_hass(hass) - result = await mock_config_entry.start_reauth_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - with patch( - "uuid.uuid4", - return_value="test-uuid", - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "new-email", - CONF_PASSWORD: "new-password", - }, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - assert mock_config_entry.data == { - CONF_EMAIL: "new-email", - CONF_PASSWORD: "new-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - } - - -@pytest.mark.parametrize( - ("side_effect", "expected_error"), - [(InvalidLoginError, "invalid_auth"), (Exception, "unknown")], -) -async def test_form_exceptions( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_setup_entry: AsyncMock, - side_effect: Exception, - expected_error: str, -) -> None: - """Test we handle invalid auth.""" - mock_linear.login.side_effect = side_effect - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - await hass.async_block_till_done() - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - }, - ) - - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": expected_error} - mock_linear.login.side_effect = None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - }, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"site": "test-site-id"} - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/linear_garage_door/test_cover.py b/tests/components/linear_garage_door/test_cover.py deleted file mode 100644 index c031db88180..00000000000 --- a/tests/components/linear_garage_door/test_cover.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Test Linear Garage Door cover.""" - -from datetime import timedelta -from unittest.mock import AsyncMock - -from freezegun.api import FrozenDateTimeFactory -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.cover import ( - DOMAIN as COVER_DOMAIN, - SERVICE_CLOSE_COVER, - SERVICE_OPEN_COVER, - CoverState, -) -from homeassistant.components.linear_garage_door import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import setup_integration - -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - async_load_json_object_fixture, - snapshot_platform, -) - - -async def test_covers( - hass: HomeAssistant, - mock_linear: AsyncMock, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that data gets parsed and returned appropriately.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -async def test_open_cover( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that opening the cover works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_1"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 0 - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_2"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_close_cover( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that closing the cover works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_2"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 0 - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_garage_1"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_update_cover_state( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, -) -> None: - """Test that closing the cover works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.COVER]) - - assert hass.states.get("cover.test_garage_1").state == CoverState.OPEN - assert hass.states.get("cover.test_garage_2").state == CoverState.CLOSED - - device_states = await async_load_json_object_fixture( - hass, "get_device_state_1.json", DOMAIN - ) - mock_linear.get_device_state.side_effect = lambda device_id: device_states[ - device_id - ] - - freezer.tick(timedelta(seconds=60)) - async_fire_time_changed(hass) - - assert hass.states.get("cover.test_garage_1").state == CoverState.CLOSING - assert hass.states.get("cover.test_garage_2").state == CoverState.OPENING diff --git a/tests/components/linear_garage_door/test_diagnostics.py b/tests/components/linear_garage_door/test_diagnostics.py deleted file mode 100644 index f51bb0a366c..00000000000 --- a/tests/components/linear_garage_door/test_diagnostics.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Test diagnostics of Linear Garage Door.""" - -from unittest.mock import AsyncMock - -from syrupy.assertion import SnapshotAssertion -from syrupy.filters import props - -from homeassistant.core import HomeAssistant - -from . import setup_integration - -from tests.common import MockConfigEntry -from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.typing import ClientSessionGenerator - - -async def test_entry_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - snapshot: SnapshotAssertion, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test config entry diagnostics.""" - await setup_integration(hass, mock_config_entry, []) - result = await get_diagnostics_for_config_entry( - hass, hass_client, mock_config_entry - ) - assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py deleted file mode 100644 index 2693eda60bb..00000000000 --- a/tests/components/linear_garage_door/test_init.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Test Linear Garage Door init.""" - -from unittest.mock import AsyncMock - -from linear_garage_door import InvalidLoginError -import pytest - -from homeassistant.components.linear_garage_door.const import DOMAIN -from homeassistant.config_entries import ( - SOURCE_IGNORE, - ConfigEntryDisabler, - ConfigEntryState, -) -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from . import setup_integration - -from tests.common import MockConfigEntry - - -async def test_unload_entry( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test the unload entry.""" - - await setup_integration(hass, mock_config_entry, []) - assert mock_config_entry.state is ConfigEntryState.LOADED - - await hass.config_entries.async_unload(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert mock_config_entry.state is ConfigEntryState.NOT_LOADED - - -@pytest.mark.parametrize( - ("side_effect", "entry_state"), - [ - ( - InvalidLoginError( - "Login provided is invalid, please check the email and password" - ), - ConfigEntryState.SETUP_ERROR, - ), - (InvalidLoginError("Invalid login"), ConfigEntryState.SETUP_RETRY), - ], -) -async def test_setup_failure( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, - side_effect: Exception, - entry_state: ConfigEntryState, -) -> None: - """Test reauth trigger setup.""" - - mock_linear.login.side_effect = side_effect - - await setup_integration(hass, mock_config_entry, []) - assert mock_config_entry.state == entry_state - - -async def test_repair_issue( - hass: HomeAssistant, - mock_linear: AsyncMock, - issue_registry: ir.IssueRegistry, -) -> None: - """Test the Linear Garage Door configuration entry loading/unloading handles the repair.""" - config_entry_1 = MockConfigEntry( - domain=DOMAIN, - entry_id="acefdd4b3a4a0911067d1cf51414201e", - title="test-site-name", - data={ - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) - await setup_integration(hass, config_entry_1, []) - assert config_entry_1.state is ConfigEntryState.LOADED - - # Add a second one - config_entry_2 = MockConfigEntry( - domain=DOMAIN, - entry_id="acefdd4b3a4a0911067d1cf51414201f", - title="test-site-name", - data={ - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - "site_id": "test-site-id", - "device_id": "test-uuid", - }, - ) - await setup_integration(hass, config_entry_2, []) - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - - # Add an ignored entry - config_entry_3 = MockConfigEntry( - source=SOURCE_IGNORE, - domain=DOMAIN, - ) - config_entry_3.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_3.entry_id) - await hass.async_block_till_done() - - assert config_entry_3.state is ConfigEntryState.NOT_LOADED - - # Add a disabled entry - config_entry_4 = MockConfigEntry( - disabled_by=ConfigEntryDisabler.USER, - domain=DOMAIN, - ) - config_entry_4.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_4.entry_id) - await hass.async_block_till_done() - - assert config_entry_4.state is ConfigEntryState.NOT_LOADED - - # Remove the first one - await hass.config_entries.async_remove(config_entry_1.entry_id) - await hass.async_block_till_done() - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - # Remove the second one - await hass.config_entries.async_remove(config_entry_2.entry_id) - await hass.async_block_till_done() - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.NOT_LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None - - # Check the ignored and disabled entries are removed - assert not hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/linear_garage_door/test_light.py b/tests/components/linear_garage_door/test_light.py deleted file mode 100644 index 1985b27aacd..00000000000 --- a/tests/components/linear_garage_door/test_light.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Test Linear Garage Door light.""" - -from datetime import timedelta -from unittest.mock import AsyncMock - -from freezegun.api import FrozenDateTimeFactory -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.light import ( - DOMAIN as LIGHT_DOMAIN, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, -) -from homeassistant.components.linear_garage_door import DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_BRIGHTNESS, - STATE_OFF, - STATE_ON, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import setup_integration - -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - async_load_json_object_fixture, - snapshot_platform, -) - - -async def test_data( - hass: HomeAssistant, - mock_linear: AsyncMock, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that data gets parsed and returned appropriately.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -async def test_turn_on( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that turning on the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_garage_2_light"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_turn_on_with_brightness( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that turning on the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_garage_2_light", CONF_BRIGHTNESS: 50}, - blocking=True, - ) - - mock_linear.operate_device.assert_called_once_with( - "test2", "Light", "DimPercent:20" - ) - - -async def test_turn_off( - hass: HomeAssistant, mock_linear: AsyncMock, mock_config_entry: MockConfigEntry -) -> None: - """Test that turning off the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_garage_1_light"}, - blocking=True, - ) - - assert mock_linear.operate_device.call_count == 1 - - -async def test_update_light_state( - hass: HomeAssistant, - mock_linear: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, -) -> None: - """Test that turning off the light works as intended.""" - - await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) - - assert hass.states.get("light.test_garage_1_light").state == STATE_ON - assert hass.states.get("light.test_garage_2_light").state == STATE_OFF - - device_states = await async_load_json_object_fixture( - hass, "get_device_state_1.json", DOMAIN - ) - mock_linear.get_device_state.side_effect = lambda device_id: device_states[ - device_id - ] - - freezer.tick(timedelta(seconds=60)) - async_fire_time_changed(hass) - - assert hass.states.get("light.test_garage_1_light").state == STATE_OFF - assert hass.states.get("light.test_garage_2_light").state == STATE_ON diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 7d1c39d10f0..a5b8e47ef2f 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -155,7 +155,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -187,7 +192,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/mastodon/snapshots/test_diagnostics.ambr b/tests/components/mastodon/snapshots/test_diagnostics.ambr index 9198410f066..ec9da1836bc 100644 --- a/tests/components/mastodon/snapshots/test_diagnostics.ambr +++ b/tests/components/mastodon/snapshots/test_diagnostics.ambr @@ -45,6 +45,7 @@ 'limited': None, 'locked': False, 'memorial': None, + 'moved': None, 'moved_to_account': None, 'mute_expires_at': None, 'noindex': False, diff --git a/tests/components/mastodon/test_notify.py b/tests/components/mastodon/test_notify.py deleted file mode 100644 index 4242f88d34a..00000000000 --- a/tests/components/mastodon/test_notify.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Tests for the Mastodon notify platform.""" - -from unittest.mock import AsyncMock - -from mastodon.Mastodon import MastodonAPIError -import pytest -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er - -from . import setup_integration - -from tests.common import MockConfigEntry - - -async def test_notify( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - entity_registry: er.EntityRegistry, - mock_mastodon_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test sending a message.""" - await setup_integration(hass, mock_config_entry) - - assert hass.services.has_service(NOTIFY_DOMAIN, "trwnh_mastodon_social") - - await hass.services.async_call( - NOTIFY_DOMAIN, - "trwnh_mastodon_social", - { - "message": "test toot", - }, - blocking=True, - return_response=False, - ) - - assert mock_mastodon_client.status_post.assert_called_once - - -async def test_notify_failed( - hass: HomeAssistant, - mock_mastodon_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test the notify raising an error.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - mock_mastodon_client.status_post.side_effect = MastodonAPIError - - with pytest.raises(HomeAssistantError, match="Unable to send message"): - await hass.services.async_call( - NOTIFY_DOMAIN, - "trwnh_mastodon_social", - { - "message": "test toot", - }, - blocking=True, - return_response=False, - ) diff --git a/tests/components/matter/fixtures/nodes/silabs_dishwasher.json b/tests/components/matter/fixtures/nodes/silabs_dishwasher.json index d0efcc7e004..fa66f4dfeef 100644 --- a/tests/components/matter/fixtures/nodes/silabs_dishwasher.json +++ b/tests/components/matter/fixtures/nodes/silabs_dishwasher.json @@ -588,31 +588,9 @@ "10": 101 } ], - "2/144/4": 120000, - "2/144/5": 0, - "2/144/6": 0, - "2/144/7": 0, "2/144/8": 0, - "2/144/9": 0, - "2/144/10": 0, "2/144/11": 120000, "2/144/12": 0, - "2/144/13": 0, - "2/144/14": 60, - "2/144/15": [ - { - "0": 1, - "1": 100000 - } - ], - "2/144/16": [ - { - "0": 1, - "1": 100000 - } - ], - "2/144/17": 9800, - "2/144/18": 0, "2/144/65532": 31, "2/144/65533": 1, "2/144/65528": [], diff --git a/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json b/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json index 3b1ed0043de..93ba7e2e026 100644 --- a/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json +++ b/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json @@ -832,31 +832,9 @@ "10": 129 } ], - "2/144/4": 120000, - "2/144/5": 0, - "2/144/6": 0, - "2/144/7": 0, "2/144/8": 0, - "2/144/9": 0, - "2/144/10": 0, "2/144/11": 120000, "2/144/12": 0, - "2/144/13": 0, - "2/144/14": 60, - "2/144/15": [ - { - "0": 1, - "1": 100000 - } - ], - "2/144/16": [ - { - "0": 1, - "1": 100000 - } - ], - "2/144/17": 9800, - "2/144/18": 0, "2/144/65532": 31, "2/144/65533": 1, "2/144/65528": [], diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index 7e2f1e7618e..da199afd3a6 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -519,7 +519,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'dishwasher_alarm_door', + 'translation_key': 'alarm_door', 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-DishwasherAlarmDoorError-93-2', 'unit_of_measurement': None, }) @@ -587,6 +587,55 @@ 'state': 'off', }) # --- +# name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charger_supply_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.evse_charger_supply_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charger supply state', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'evse_supply_state', + 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseSupplyStateSensor-153-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charger_supply_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'evse Charger supply state', + }), + 'context': , + 'entity_id': 'binary_sensor.evse_charger_supply_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charging_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -685,7 +734,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charger_supply_state-entry] +# name: test_binary_sensors[silabs_refrigerator][binary_sensor.refrigerator_door_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -697,8 +746,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.evse_charger_supply_state', + 'entity_category': , + 'entity_id': 'binary_sensor.refrigerator_door_alarm', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -708,30 +757,30 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Charger supply state', + 'original_name': 'Door alarm', 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'evse_supply_state', - 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseSupplyStateSensor-153-1', + 'translation_key': 'alarm_door', + 'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-1-RefrigeratorAlarmDoorOpen-87-2', 'unit_of_measurement': None, }) # --- -# name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charger_supply_state-state] +# name: test_binary_sensors[silabs_refrigerator][binary_sensor.refrigerator_door_alarm-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'running', - 'friendly_name': 'evse Charger supply state', + 'device_class': 'problem', + 'friendly_name': 'Refrigerator Door alarm', }), 'context': , - 'entity_id': 'binary_sensor.evse_charger_supply_state', + 'entity_id': 'binary_sensor.refrigerator_door_alarm', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- # name: test_binary_sensors[silabs_water_heater][binary_sensor.water_heater_boost_state-entry] @@ -1075,3 +1124,150 @@ 'state': 'off', }) # --- +# name: test_binary_sensors[valve][binary_sensor.valve_general_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.valve_general_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'General fault', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_fault_general_fault', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlValveFault_GeneralFault-129-9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_general_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Valve General fault', + }), + 'context': , + 'entity_id': 'binary_sensor.valve_general_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_blocked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.valve_valve_blocked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve blocked', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_fault_blocked', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlValveFault_Blocked-129-9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_blocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Valve Valve blocked', + }), + 'context': , + 'entity_id': 'binary_sensor.valve_valve_blocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_leaking-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.valve_valve_leaking', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve leaking', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_fault_leaking', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlValveFault_Leaking-129-9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_leaking-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Valve Valve leaking', + }), + 'context': , + 'entity_id': 'binary_sensor.valve_valve_leaking', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index 2ffbd248290..f70c38f6b6d 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -2038,6 +2038,54 @@ 'state': 'unknown', }) # --- +# name: test_buttons[smoke_detector][button.smoke_sensor_self_test-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.smoke_sensor_self_test', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Self-test', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'self_test_request', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SmokeCoAlarmSelfTestRequest-92-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[smoke_detector][button.smoke_sensor_self_test-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke sensor Self-test', + }), + 'context': , + 'entity_id': 'button.smoke_sensor_self_test', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[switch_unit][button.mock_switchunit_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index 24a92799082..0273c83ac5c 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -2366,3 +2366,61 @@ 'state': '4.0', }) # --- +# name: test_numbers[valve][number.valve_default_open_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65534, + 'min': 1, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.valve_default_open_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Default open duration', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_configuration_and_control_default_open_duration', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlDefaultOpenDuration-129-1', + 'unit_of_measurement': , + }) +# --- +# name: test_numbers[valve][number.valve_default_open_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Valve Default open duration', + 'max': 65534, + 'min': 1, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.valve_default_open_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index bff4ad7909d..290016f0ff3 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -468,7 +468,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM1Sensor-1068-0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[air_purifier][sensor.air_purifier_pm1-state] @@ -477,7 +477,7 @@ 'device_class': 'pm1', 'friendly_name': 'Air Purifier PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.air_purifier_pm1', @@ -521,7 +521,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM10Sensor-1069-0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[air_purifier][sensor.air_purifier_pm10-state] @@ -530,7 +530,7 @@ 'device_class': 'pm10', 'friendly_name': 'Air Purifier PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.air_purifier_pm10', @@ -574,7 +574,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM25Sensor-1066-0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[air_purifier][sensor.air_purifier_pm2_5-state] @@ -583,7 +583,7 @@ 'device_class': 'pm25', 'friendly_name': 'Air Purifier PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.air_purifier_pm2_5', @@ -1017,7 +1017,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM1Sensor-1068-0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm1-state] @@ -1026,7 +1026,7 @@ 'device_class': 'pm1', 'friendly_name': 'lightfi-aq1-air-quality-sensor PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm1', @@ -1070,7 +1070,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM10Sensor-1069-0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm10-state] @@ -1079,7 +1079,7 @@ 'device_class': 'pm10', 'friendly_name': 'lightfi-aq1-air-quality-sensor PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm10', @@ -1123,7 +1123,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM25Sensor-1066-0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm2_5-state] @@ -1132,7 +1132,7 @@ 'device_class': 'pm25', 'friendly_name': 'lightfi-aq1-air-quality-sensor PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm2_5', @@ -1251,6 +1251,65 @@ 'state': '189.0', }) # --- +# name: test_sensors[battery_storage][sensor.mock_battery_storage_active_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_battery_storage_active_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active current', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'active_current', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ElectricalPowerMeasurementActiveCurrent-144-5', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[battery_storage][sensor.mock_battery_storage_active_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Mock Battery Storage Active current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_battery_storage_active_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_sensors[battery_storage][sensor.mock_battery_storage_appliance_energy_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1427,65 +1486,6 @@ 'state': '48.0', }) # --- -# name: test_sensors[battery_storage][sensor.mock_battery_storage_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.mock_battery_storage_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ElectricalPowerMeasurementActiveCurrent-144-5', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[battery_storage][sensor.mock_battery_storage_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Mock Battery Storage Current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.mock_battery_storage_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- # name: test_sensors[battery_storage][sensor.mock_battery_storage_energy_optimization_opt_out-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1763,7 +1763,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'voltage', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-1-ElectricalPowerMeasurementVoltage-144-4', 'unit_of_measurement': , }) @@ -2176,7 +2176,7 @@ 'state': '238.800003051758', }) # --- -# name: test_sensors[eve_energy_plug_patched][sensor.eve_energy_plug_patched_current-entry] +# name: test_sensors[eve_energy_plug_patched][sensor.eve_energy_plug_patched_active_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2191,7 +2191,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.eve_energy_plug_patched_current', + 'entity_id': 'sensor.eve_energy_plug_patched_active_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2209,26 +2209,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Current', + 'original_name': 'Active current', 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'active_current', 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', 'unit_of_measurement': , }) # --- -# name: test_sensors[eve_energy_plug_patched][sensor.eve_energy_plug_patched_current-state] +# name: test_sensors[eve_energy_plug_patched][sensor.eve_energy_plug_patched_active_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', - 'friendly_name': 'Eve Energy Plug Patched Current', + 'friendly_name': 'Eve Energy Plug Patched Active current', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.eve_energy_plug_patched_current', + 'entity_id': 'sensor.eve_energy_plug_patched_active_current', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2391,7 +2391,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'voltage', 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', 'unit_of_measurement': , }) @@ -2906,6 +2906,68 @@ 'state': '16.03', }) # --- +# name: test_sensors[eve_weather_sensor][sensor.eve_weather_weather_trend-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'sunny', + 'cloudy', + 'rainy', + 'stormy', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.eve_weather_weather_trend', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Weather trend', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'eve_weather_trend', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-EveWeatherWeatherTrend-319486977-319422485', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[eve_weather_sensor][sensor.eve_weather_weather_trend-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Eve Weather Weather trend', + 'options': list([ + 'sunny', + 'cloudy', + 'rainy', + 'stormy', + ]), + }), + 'context': , + 'entity_id': 'sensor.eve_weather_weather_trend', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'rainy', + }) +# --- # name: test_sensors[extractor_hood][sensor.mock_extractor_hood_activated_carbon_filter_condition-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4333,7 +4395,7 @@ 'state': '0.0', }) # --- -# name: test_sensors[silabs_dishwasher][sensor.dishwasher_current-entry] +# name: test_sensors[silabs_dishwasher][sensor.dishwasher_effective_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4348,7 +4410,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.dishwasher_current', + 'entity_id': 'sensor.dishwasher_effective_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4366,32 +4428,91 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Current', + 'original_name': 'Effective current', 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', + 'translation_key': 'rms_current', + 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalPowerMeasurementRMSCurrent-144-12', 'unit_of_measurement': , }) # --- -# name: test_sensors[silabs_dishwasher][sensor.dishwasher_current-state] +# name: test_sensors[silabs_dishwasher][sensor.dishwasher_effective_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', - 'friendly_name': 'Dishwasher Current', + 'friendly_name': 'Dishwasher Effective current', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.dishwasher_current', + 'entity_id': 'sensor.dishwasher_effective_current', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.0', }) # --- +# name: test_sensors[silabs_dishwasher][sensor.dishwasher_effective_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.dishwasher_effective_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Effective voltage', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rms_voltage', + 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalPowerMeasurementRMSVoltage-144-11', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_dishwasher][sensor.dishwasher_effective_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Dishwasher Effective voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dishwasher_effective_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120.0', + }) +# --- # name: test_sensors[silabs_dishwasher][sensor.dishwasher_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4574,65 +4695,6 @@ 'state': '0.0', }) # --- -# name: test_sensors[silabs_dishwasher][sensor.dishwasher_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.dishwasher_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Voltage', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[silabs_dishwasher][sensor.dishwasher_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Dishwasher Voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.dishwasher_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '120.0', - }) -# --- # name: test_sensors[silabs_evse_charging][sensor.evse_appliance_energy_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5134,65 +5196,6 @@ 'state': '32.0', }) # --- -# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.laundrywasher_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'LaundryWasher Current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.laundrywasher_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- # name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current_phase-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5253,6 +5256,124 @@ 'state': 'pre-soak', }) # --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_effective_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.laundrywasher_effective_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Effective current', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rms_current', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementRMSCurrent-144-12', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_effective_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'LaundryWasher Effective current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_effective_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_effective_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.laundrywasher_effective_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Effective voltage', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rms_voltage', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementRMSVoltage-144-11', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_effective_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'LaundryWasher Effective voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_effective_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120.0', + }) +# --- # name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5433,7 +5554,7 @@ 'state': '0.0', }) # --- -# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_voltage-entry] +# name: test_sensors[silabs_water_heater][sensor.water_heater_active_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -5448,7 +5569,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.laundrywasher_voltage', + 'entity_id': 'sensor.water_heater_active_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5458,38 +5579,38 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 0, + 'suggested_display_precision': 2, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': , }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Voltage', + 'original_name': 'Active current', 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', - 'unit_of_measurement': , + 'translation_key': 'active_current', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', + 'unit_of_measurement': , }) # --- -# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_voltage-state] +# name: test_sensors[silabs_water_heater][sensor.water_heater_active_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'LaundryWasher Voltage', + 'device_class': 'current', + 'friendly_name': 'Water Heater Active current', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.laundrywasher_voltage', + 'entity_id': 'sensor.water_heater_active_current', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '120.0', + 'state': '0.1', }) # --- # name: test_sensors[silabs_water_heater][sensor.water_heater_appliance_energy_state-entry] @@ -5556,65 +5677,6 @@ 'state': 'online', }) # --- -# name: test_sensors[silabs_water_heater][sensor.water_heater_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.water_heater_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[silabs_water_heater][sensor.water_heater_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Water Heater Current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.water_heater_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.1', - }) -# --- # name: test_sensors[silabs_water_heater][sensor.water_heater_energy_optimization_opt_out-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5941,7 +6003,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'voltage', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', 'unit_of_measurement': , }) @@ -6122,7 +6184,7 @@ 'state': '0.0', }) # --- -# name: test_sensors[solar_power][sensor.solarpower_current-entry] +# name: test_sensors[solar_power][sensor.solarpower_active_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -6137,7 +6199,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.solarpower_current', + 'entity_id': 'sensor.solarpower_active_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -6155,26 +6217,26 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Current', + 'original_name': 'Active current', 'platform': 'matter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'active_current', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementActiveCurrent-144-5', 'unit_of_measurement': , }) # --- -# name: test_sensors[solar_power][sensor.solarpower_current-state] +# name: test_sensors[solar_power][sensor.solarpower_active_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', - 'friendly_name': 'SolarPower Current', + 'friendly_name': 'SolarPower Active current', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.solarpower_current', + 'entity_id': 'sensor.solarpower_active_current', 'last_changed': , 'last_reported': , 'last_updated': , @@ -6337,7 +6399,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'voltage', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementVoltage-144-4', 'unit_of_measurement': , }) diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index fcfd4da84c8..06055af8c9d 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -275,3 +275,72 @@ async def test_dishwasher_alarm( state = hass.states.get("binary_sensor.dishwasher_door_alarm") assert state assert state.state == "on" + + +@pytest.mark.parametrize("node_fixture", ["valve"]) +async def test_water_valve( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test valve alarms.""" + # ValveFault default state + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "off" + + # ValveFault general_fault test + set_node_attribute(matter_node, 1, 129, 9, 1) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "on" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "off" + + # ValveFault valve_blocked test + set_node_attribute(matter_node, 1, 129, 9, 2) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "on" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "off" + + # ValveFault valve_leaking test + set_node_attribute(matter_node, 1, 129, 9, 4) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "on" diff --git a/tests/components/matter/test_button.py b/tests/components/matter/test_button.py index 2af2d40cb74..6452dabc10d 100644 --- a/tests/components/matter/test_button.py +++ b/tests/components/matter/test_button.py @@ -80,3 +80,30 @@ async def test_operational_state_buttons( endpoint_id=1, command=clusters.OperationalState.Commands.Pause(), ) + + +@pytest.mark.parametrize("node_fixture", ["smoke_detector"]) +async def test_smoke_detector_self_test( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test button entity is created for a Matter SmokeCoAlarm Cluster.""" + state = hass.states.get("button.smoke_sensor_self_test") + assert state + assert state.attributes["friendly_name"] == "Smoke sensor Self-test" + # test press action + await hass.services.async_call( + "button", + "press", + { + "entity_id": "button.smoke_sensor_self_test", + }, + blocking=True, + ) + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.SmokeCoAlarm.Commands.SelfTestRequest(), + ) diff --git a/tests/components/matter/test_valve.py b/tests/components/matter/test_valve.py index 36ab34cb64e..db64a5bacef 100644 --- a/tests/components/matter/test_valve.py +++ b/tests/components/matter/test_valve.py @@ -133,3 +133,22 @@ async def test_valve( command=clusters.ValveConfigurationAndControl.Commands.Open(targetLevel=100), ) matter_client.send_device_command.reset_mock() + + # test using set_position action to close valve + await hass.services.async_call( + "valve", + "set_valve_position", + { + "entity_id": entity_id, + "position": 0, + }, + blocking=True, + ) + + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.ValveConfigurationAndControl.Commands.Close(), + ) + matter_client.send_device_command.reset_mock() diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index ae3a84e66a0..e82f1cd3612 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -161,7 +161,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -193,7 +198,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index d1dc03ed12a..2b585319826 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -1,5 +1,8 @@ """The tests for the media_player platform.""" +import math +from unittest.mock import patch + import pytest from homeassistant.components.media_player import ( @@ -13,12 +16,17 @@ from homeassistant.components.media_player import ( SERVICE_VOLUME_SET, BrowseMedia, MediaClass, + MediaPlayerEntity, MediaType, SearchMedia, intent as media_player_intent, ) -from homeassistant.components.media_player.const import MediaPlayerEntityFeature +from homeassistant.components.media_player.const import ( + MediaPlayerEntityFeature, + MediaPlayerState, +) from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, STATE_IDLE, STATE_PAUSED, @@ -32,8 +40,10 @@ from homeassistant.helpers import ( floor_registry as fr, intent, ) +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.setup import async_setup_component -from tests.common import async_mock_service +from tests.common import MockEntityPlatform, async_mock_service async def test_pause_media_player_intent(hass: HomeAssistant) -> None: @@ -873,3 +883,165 @@ async def test_search_and_play_media_player_intent_with_media_class( "media_class": {"value": "invalid_class"}, }, ) + + +@pytest.mark.parametrize( + ("direction", "volume_change", "volume_change_int"), + [("up", 0.1, 20), ("down", -0.1, -20)], +) +async def test_volume_relative_media_player_intent( + hass: HomeAssistant, direction: str, volume_change: float, volume_change_int: int +) -> None: + """Test relative volume intents for media players.""" + assert await async_setup_component(hass, DOMAIN, {}) + await media_player_intent.async_setup_intents(hass) + + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] + + default_volume = 0.5 + + class VolumeTestMediaPlayer(MediaPlayerEntity): + _attr_supported_features = MediaPlayerEntityFeature.VOLUME_SET + _attr_volume_level = default_volume + _attr_volume_step = 0.1 + _attr_state = MediaPlayerState.IDLE + + async def async_set_volume_level(self, volume): + self._attr_volume_level = volume + + idle_entity = VolumeTestMediaPlayer() + idle_entity.hass = hass + idle_entity.platform = MockEntityPlatform(hass) + idle_entity.entity_id = f"{DOMAIN}.idle_media_player" + await component.async_add_entities([idle_entity]) + + hass.states.async_set( + idle_entity.entity_id, + STATE_IDLE, + attributes={ + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_SET, + ATTR_FRIENDLY_NAME: "Idle Media Player", + }, + ) + + idle_expected_volume = default_volume + + # Only 1 media player is present, so it's targeted even though its idle + assert idle_entity.volume_level is not None + assert math.isclose(idle_entity.volume_level, idle_expected_volume) + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME_RELATIVE, + {"volume_step": {"value": direction}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + idle_expected_volume += volume_change + assert math.isclose(idle_entity.volume_level, idle_expected_volume) + + # Multiple media players (playing one should be targeted) + playing_entity = VolumeTestMediaPlayer() + playing_entity.hass = hass + playing_entity.platform = MockEntityPlatform(hass) + playing_entity.entity_id = f"{DOMAIN}.playing_media_player" + await component.async_add_entities([playing_entity]) + + hass.states.async_set( + playing_entity.entity_id, + STATE_PLAYING, + attributes={ + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_SET, + ATTR_FRIENDLY_NAME: "Playing Media Player", + }, + ) + + playing_expected_volume = default_volume + assert playing_entity.volume_level is not None + assert math.isclose(playing_entity.volume_level, playing_expected_volume) + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME_RELATIVE, + {"volume_step": {"value": direction}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + playing_expected_volume += volume_change + assert math.isclose(idle_entity.volume_level, idle_expected_volume) + assert math.isclose(playing_entity.volume_level, playing_expected_volume) + + # We can still target by name even if the media player is idle + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME_RELATIVE, + {"volume_step": {"value": direction}, "name": {"value": "Idle media player"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + idle_expected_volume += volume_change + assert math.isclose(idle_entity.volume_level, idle_expected_volume) + assert math.isclose(playing_entity.volume_level, playing_expected_volume) + + # Set relative volume by percent + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME_RELATIVE, + {"volume_step": {"value": volume_change_int}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + playing_expected_volume += volume_change_int / 100 + assert math.isclose(idle_entity.volume_level, idle_expected_volume) + assert math.isclose(playing_entity.volume_level, playing_expected_volume) + + # Test error in method + with ( + patch.object( + playing_entity, "async_volume_up", side_effect=RuntimeError("boom!") + ), + pytest.raises(intent.IntentError), + ): + await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME_RELATIVE, + {"volume_step": {"value": "up"}}, + ) + + # Multiple idle media players should not match + hass.states.async_set( + playing_entity.entity_id, + STATE_IDLE, + attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_SET}, + ) + + with pytest.raises(intent.MatchFailedError): + await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME_RELATIVE, + {"volume_step": {"value": direction}}, + ) + + # Test feature not supported + for entity_id in (idle_entity.entity_id, playing_entity.entity_id): + hass.states.async_set( + entity_id, + STATE_PLAYING, + attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature(0)}, + ) + + with pytest.raises(intent.MatchFailedError): + await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_SET_VOLUME_RELATIVE, + {"volume_step": {"value": direction}}, + ) diff --git a/tests/components/miele/conftest.py b/tests/components/miele/conftest.py index d91485ffc59..c8a47eb2b59 100644 --- a/tests/components/miele/conftest.py +++ b/tests/components/miele/conftest.py @@ -117,7 +117,7 @@ def mock_miele_client( """Mock a Miele client.""" with patch( - "homeassistant.components.miele.AsyncConfigEntryAuth", + "homeassistant.components.miele.MieleAPI", autospec=True, ) as mock_client: client = mock_client.return_value diff --git a/tests/components/miele/fixtures/coffee_system.json b/tests/components/miele/fixtures/coffee_system.json new file mode 100644 index 00000000000..36039e7be7f --- /dev/null +++ b/tests/components/miele/fixtures/coffee_system.json @@ -0,0 +1,126 @@ +{ + "DummyAppliance_CoffeeSystem": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 17, + "value_localized": "Coffee machine" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "11", + "techType": "CM6160", + "matNumber": "11488670", + "swids": [] + }, + "xkmIdentLabel": { + "techType": "EK037", + "releaseVersion": "04.05" + } + }, + "state": { + "ProgramID": { + "value_raw": 24001, + "value_localized": "espresso", + "key_localized": "Program name" + }, + "status": { + "value_raw": 5, + "value_localized": "In use", + "key_localized": "status" + }, + "programType": { + "value_raw": 1, + "value_localized": "Program", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 4353, + "value_localized": "Espresso", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": null, + "light": 1, + "elapsedTime": [], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } + } +} diff --git a/tests/components/miele/fixtures/laundry.json b/tests/components/miele/fixtures/laundry.json new file mode 100644 index 00000000000..a72f2283039 --- /dev/null +++ b/tests/components/miele/fixtures/laundry.json @@ -0,0 +1,272 @@ +{ + "DummyWasher": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 1, + "value_localized": "Washing machine" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "32", + "techType": "WCR870", + "matNumber": "10979100", + "swids": [ + "5836", + "20457", + "20449", + "25260", + "20450", + "5013", + "25314", + "25205", + "25313", + "25191" + ] + }, + "xkmIdentLabel": { + "techType": "EK057", + "releaseVersion": "08.32" + } + }, + "state": { + "ProgramID": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": true, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [0, 0], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } + }, + "DummyDryer": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 2, + "value_localized": "Tumble dryer" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "13", + "techType": "TCJ690WP", + "matNumber": "10979980", + "swids": [ + "5213", + "25359", + "25360", + "25002", + "20456", + "25213", + "5136", + "20445", + "25234", + "4174" + ] + }, + "xkmIdentLabel": { + "techType": "EK037", + "releaseVersion": "04.05" + } + }, + "state": { + "ProgramID": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": true, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [0, 55], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } + } +} diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 2805a683077..f385a53b6e4 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1,4 +1,354 @@ # serializer version: 1 +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.coffee_system', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:coffee-maker', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_CoffeeSystem-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Coffee system', + 'icon': 'mdi:coffee-maker', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.coffee_system', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'in_use', + }) +# --- +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'appliance_rinse', + 'appliance_settings', + 'barista_assistant', + 'black_tea', + 'brewing_unit_degrease', + 'cafe_au_lait', + 'caffe_latte', + 'cappuccino', + 'cappuccino_italiano', + 'check_appliance', + 'coffee', + 'coffee_pot', + 'descaling', + 'espresso', + 'espresso_macchiato', + 'flat_white', + 'fruit_tea', + 'green_tea', + 'herbal_tea', + 'hot_milk', + 'hot_water', + 'japanese_tea', + 'latte_macchiato', + 'long_coffee', + 'milk_foam', + 'milk_pipework_clean', + 'milk_pipework_rinse', + 'no_program', + 'ristretto', + 'very_hot_water', + 'white_tea', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.coffee_system_program', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Program', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'program_id', + 'unique_id': 'DummyAppliance_CoffeeSystem-state_program_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Coffee system Program', + 'options': list([ + 'appliance_rinse', + 'appliance_settings', + 'barista_assistant', + 'black_tea', + 'brewing_unit_degrease', + 'cafe_au_lait', + 'caffe_latte', + 'cappuccino', + 'cappuccino_italiano', + 'check_appliance', + 'coffee', + 'coffee_pot', + 'descaling', + 'espresso', + 'espresso_macchiato', + 'flat_white', + 'fruit_tea', + 'green_tea', + 'herbal_tea', + 'hot_milk', + 'hot_water', + 'japanese_tea', + 'latte_macchiato', + 'long_coffee', + 'milk_foam', + 'milk_pipework_clean', + 'milk_pipework_rinse', + 'no_program', + 'ristretto', + 'very_hot_water', + 'white_tea', + ]), + 'profile': 'profile_1', + }), + 'context': , + 'entity_id': 'sensor.coffee_system_program', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'espresso', + }) +# --- +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '2nd_espresso', + '2nd_grinding', + '2nd_pre_brewing', + 'dispensing', + 'espresso', + 'grinding', + 'heating_up', + 'hot_milk', + 'milk_foam', + 'not_running', + 'pre_brewing', + 'rinse', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.coffee_system_program_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Program phase', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'program_phase', + 'unique_id': 'DummyAppliance_CoffeeSystem-state_program_phase', + 'unit_of_measurement': None, + }) +# --- +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Coffee system Program phase', + 'options': list([ + '2nd_espresso', + '2nd_grinding', + '2nd_pre_brewing', + 'dispensing', + 'espresso', + 'grinding', + 'heating_up', + 'hot_milk', + 'milk_foam', + 'not_running', + 'pre_brewing', + 'rinse', + ]), + }), + 'context': , + 'entity_id': 'sensor.coffee_system_program_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'espresso', + }) +# --- +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'automatic_program', + 'cleaning_care_program', + 'maintenance_program', + 'normal_operation_mode', + 'own_program', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.coffee_system_program_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Program type', + 'platform': 'miele', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'program_type', + 'unique_id': 'DummyAppliance_CoffeeSystem-state_program_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_coffee_system_sensor_states[platforms0-coffee_system.json][sensor.coffee_system_program_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Coffee system Program type', + 'options': list([ + 'automatic_program', + 'cleaning_care_program', + 'maintenance_program', + 'normal_operation_mode', + 'own_program', + ]), + }), + 'context': , + 'entity_id': 'sensor.coffee_system_program_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'own_program', + }) +# --- # name: test_fan_hob_sensor_states[platforms0-fan_devices.json][sensor.hob_with_extraction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -208,6 +558,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -266,6 +617,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -304,6 +656,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -362,6 +715,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -400,6 +754,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -458,6 +813,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -496,6 +852,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -554,6 +911,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -592,6 +950,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -650,6 +1009,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -688,6 +1048,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -746,6 +1107,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -784,6 +1146,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -842,6 +1205,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -880,6 +1244,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -938,6 +1303,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -976,6 +1342,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1034,6 +1401,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1457,6 +1825,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1515,6 +1884,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1553,6 +1923,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1611,6 +1982,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1649,6 +2021,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1707,6 +2080,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1745,6 +2119,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1803,6 +2178,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1841,6 +2217,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -1899,6 +2276,7 @@ 'plate_step_8', 'plate_step_9', 'plate_step_boost', + 'plate_step_boost_2', 'plate_step_warming', ]), }), @@ -2345,7 +2723,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_sensor_states[platforms0][sensor.oven_program-entry] @@ -3351,7 +3729,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_sensor_states[platforms0][sensor.washing_machine_energy_consumption-entry] @@ -5497,7 +5875,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_consumption-entry] @@ -6393,7 +6771,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_vacuum_sensor_states[platforms0-vacuum_device.json][sensor.robot_vacuum_cleaner_program-entry] diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index e5051a683c9..f8d620c8bd0 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -239,7 +239,6 @@ async def test_temperature_sensor_registry_lookup( await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == "unknown" @@ -271,3 +270,321 @@ async def test_fan_hob_sensor_states( """Test robot fan / hob sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("load_device_file", ["coffee_system.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_coffee_system_sensor_states( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: None, +) -> None: + """Test coffee system sensor state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("load_device_file", ["laundry.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +async def test_laundry_wash_scenario( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + mock_config_entry: MockConfigEntry, + device_fixture: MieleDevices, + freezer: FrozenDateTimeFactory, +) -> None: + """Parametrized test for verifying time sensors for wahsing machine devices when API glitches at program end.""" + + step = 0 + + # Initial state when the washing machine is off + check_sensor_state(hass, "sensor.washing_machine", "off", step) + check_sensor_state(hass, "sensor.washing_machine_program", "no_program", step) + check_sensor_state( + hass, "sensor.washing_machine_program_phase", "not_running", step + ) + check_sensor_state( + hass, "sensor.washing_machine_target_temperature", "unknown", step + ) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "unknown", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "0", step) + # OFF -> elapsed forced to unknown (some devices continue reporting last value of last cycle) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "unknown", step) + + # Simulate program started + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 5 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "In use" + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_raw"] = 3 + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_localized"] = ( + "Minimum iron" + ) + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 260 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Main wash" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 45 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0]["value_raw"] = 3000 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0][ + "value_localized" + ] = 30.0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 12 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_raw"] = 1200 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_localized"] = "1200" + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "in_use", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.washing_machine_program_phase", "main_wash", step) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "30.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "105", step) + # IN_USE -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "12", step) + + # Simulate rinse hold phase + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 11 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "Rinse hold" + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 262 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Rinse hold" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 8 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 49 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "rinse_hold", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.washing_machine_program_phase", "rinse_hold", step) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "30.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "8", step) + # RINSE HOLD -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "109", step) + + # Simulate program ended + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 7 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "Finished" + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 267 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Anti-crease" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 0 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "program_ended", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state( + hass, "sensor.washing_machine_program_phase", "anti_crease", step + ) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "30.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "0", step) + # PROGRAM_ENDED -> elapsed time kept from last program (some devices immediately go to 0) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "109", step) + + # Simulate when door is opened after program ended + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 3 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = ( + "Programme selected" + ) + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 256 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = "" + device_fixture["DummyWasher"]["state"]["targetTemperature"][0]["value_raw"] = 4000 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0][ + "value_localized" + ] = 40.0 + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 59 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 0 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "programmed", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state( + hass, "sensor.washing_machine_program_phase", "not_running", step + ) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "40.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "119", step) + # PROGRAMMED -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "0", step) + + +@pytest.mark.parametrize("load_device_file", ["laundry.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +async def test_laundry_dry_scenario( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + mock_config_entry: MockConfigEntry, + device_fixture: MieleDevices, + freezer: FrozenDateTimeFactory, +) -> None: + """Parametrized test for verifying time sensors for tumble dryer devices when API reports time value from last cycle, when device is off.""" + + step = 0 + + # Initial state when the washing machine is off + check_sensor_state(hass, "sensor.tumble_dryer", "off", step) + check_sensor_state(hass, "sensor.tumble_dryer_program", "no_program", step) + check_sensor_state(hass, "sensor.tumble_dryer_program_phase", "not_running", step) + check_sensor_state(hass, "sensor.tumble_dryer_drying_step", "unknown", step) + check_sensor_state(hass, "sensor.tumble_dryer_remaining_time", "0", step) + # OFF -> elapsed forced to unknown (some devices continue reporting last value of last cycle) + check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "unknown", step) + + # Simulate program started + device_fixture["DummyDryer"]["state"]["status"]["value_raw"] = 5 + device_fixture["DummyDryer"]["state"]["status"]["value_localized"] = "In use" + device_fixture["DummyDryer"]["state"]["ProgramID"]["value_raw"] = 3 + device_fixture["DummyDryer"]["state"]["ProgramID"]["value_localized"] = ( + "Minimum iron" + ) + device_fixture["DummyDryer"]["state"]["programPhase"]["value_raw"] = 514 + device_fixture["DummyDryer"]["state"]["programPhase"]["value_localized"] = "Drying" + device_fixture["DummyDryer"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyDryer"]["state"]["remainingTime"][1] = 49 + device_fixture["DummyDryer"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyDryer"]["state"]["elapsedTime"][1] = 20 + device_fixture["DummyDryer"]["state"]["dryingStep"]["value_raw"] = 2 + device_fixture["DummyDryer"]["state"]["dryingStep"]["value_localized"] = "Normal" + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.tumble_dryer", "in_use", step) + check_sensor_state(hass, "sensor.tumble_dryer_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.tumble_dryer_program_phase", "drying", step) + check_sensor_state(hass, "sensor.tumble_dryer_drying_step", "normal", step) + check_sensor_state(hass, "sensor.tumble_dryer_remaining_time", "49", step) + # IN_USE -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "20", step) + + # Simulate program end + device_fixture["DummyDryer"]["state"]["status"]["value_raw"] = 7 + device_fixture["DummyDryer"]["state"]["status"]["value_localized"] = "Finished" + device_fixture["DummyDryer"]["state"]["programPhase"]["value_raw"] = 522 + device_fixture["DummyDryer"]["state"]["programPhase"]["value_localized"] = ( + "Finished" + ) + device_fixture["DummyDryer"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyDryer"]["state"]["remainingTime"][1] = 0 + device_fixture["DummyDryer"]["state"]["elapsedTime"][0] = 1 + device_fixture["DummyDryer"]["state"]["elapsedTime"][1] = 18 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.tumble_dryer", "program_ended", step) + check_sensor_state(hass, "sensor.tumble_dryer_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.tumble_dryer_program_phase", "finished", step) + check_sensor_state(hass, "sensor.tumble_dryer_drying_step", "normal", step) + check_sensor_state(hass, "sensor.tumble_dryer_remaining_time", "0", step) + # PROGRAM_ENDED -> elapsed time kept from last program (some devices immediately go to 0) + check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "20", step) + + +@pytest.mark.parametrize("load_device_file", ["laundry.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +async def test_elapsed_time_sensor_restored( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_miele_client: MagicMock, + setup_platform: None, + device_fixture: MieleDevices, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that elapsed time returns the restored value when program ended.""" + + entity_id = "sensor.washing_machine_elapsed_time" + + # Simulate program started + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 5 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "In use" + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_raw"] = 3 + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_localized"] = ( + "Minimum iron" + ) + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 260 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Main wash" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 45 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0]["value_raw"] = 3000 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0][ + "value_localized" + ] = 30.0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 12 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_raw"] = 1200 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_localized"] = "1200" + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "12" + + # Simulate program ended + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 7 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "Finished" + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 267 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Anti-crease" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 0 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # unload config entry and reload to make sure that the state is restored + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "unavailable" + + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # check that elapsed time is the one restored and not the value reported by API (0) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "12" diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index a35cc95605d..f7bd4b13a1b 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -23,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from homeassistant.util.hass_dict import HassKey from tests.common import async_fire_time_changed, mock_restore_cache @@ -121,6 +122,7 @@ def mock_pymodbus_fixture(do_exception, register_words): async def mock_modbus_fixture( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, check_config_loaded, config_addon, do_config, @@ -158,6 +160,15 @@ async def mock_modbus_fixture( result = await async_setup_component(hass, DOMAIN, config) assert result or not check_config_loaded await hass.async_block_till_done() + key = HassKey(DOMAIN) + if key not in hass.data: + return None + hub = hass.data[HassKey(DOMAIN)][TEST_MODBUS_NAME] + await hub.event_connected.wait() + assert hub.event_connected.is_set() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() return mock_pymodbus diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 54d4c5f6666..f661dd2083c 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -467,7 +467,7 @@ async def test_hvac_onoff_values(hass: HomeAssistant, mock_modbus) -> None: ) await hass.async_block_till_done() - mock_modbus.write_register.assert_called_with(11, value=0xAA, slave=10) + mock_modbus.write_register.assert_called_with(11, value=0xAA, device_id=10) await hass.services.async_call( CLIMATE_DOMAIN, @@ -477,7 +477,7 @@ async def test_hvac_onoff_values(hass: HomeAssistant, mock_modbus) -> None: ) await hass.async_block_till_done() - mock_modbus.write_register.assert_called_with(11, value=0xFF, slave=10) + mock_modbus.write_register.assert_called_with(11, value=0xFF, device_id=10) @pytest.mark.parametrize( @@ -506,7 +506,7 @@ async def test_hvac_onoff_coil(hass: HomeAssistant, mock_modbus) -> None: ) await hass.async_block_till_done() - mock_modbus.write_coil.assert_called_with(11, value=1, slave=10) + mock_modbus.write_coil.assert_called_with(11, value=1, device_id=10) await hass.services.async_call( CLIMATE_DOMAIN, @@ -516,7 +516,7 @@ async def test_hvac_onoff_coil(hass: HomeAssistant, mock_modbus) -> None: ) await hass.async_block_till_done() - mock_modbus.write_coil.assert_called_with(11, value=0, slave=10) + mock_modbus.write_coil.assert_called_with(11, value=0, device_id=10) @pytest.mark.parametrize( @@ -794,6 +794,140 @@ async def test_hvac_onoff_coil_update( assert state.state == result +@pytest.mark.parametrize( + ( + "do_config", + "result_before", + "coil_value_before", + "result_after", + "coil_value_after", + ), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_HVAC_ONOFF_COIL: 11, + }, + ] + }, + HVACMode.OFF, + [0x00], + HVACMode.AUTO, + [0x01], + ), + ], +) +async def test_hvac_onoff_coil_transition_update( + hass: HomeAssistant, + mock_modbus_ha, + result_before, + coil_value_before, + result_after, + coil_value_after, +) -> None: + """Test climate update based on On/Off coil values without hvacmode register.""" + mock_modbus_ha.read_coils.return_value = ReadResult(coil_value_before) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == result_before + + mock_modbus_ha.read_coils.return_value = ReadResult(coil_value_after) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == result_after + + +@pytest.mark.parametrize( + ( + "do_config", + "result_before", + "register_value_before", + "result_after", + "register_value_after", + ), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_HVAC_ONOFF_REGISTER: 11, + }, + ] + }, + HVACMode.OFF, + [0x00], + HVACMode.AUTO, + [0x01], + ), + ], +) +async def test_hvac_onoff_register_transition_update( + hass: HomeAssistant, + mock_modbus_ha, + result_before, + register_value_before, + result_after, + register_value_after, +) -> None: + """Test climate update based on On/Off register values without hvacmode register.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult( + register_value_before + ) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == result_before + + mock_modbus_ha.read_holding_registers.return_value = ReadResult( + register_value_after + ) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == result_after + + @pytest.mark.parametrize( ("do_config", "result", "register_words"), [ diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 4c0a8bd8f6e..3816e9878cb 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -63,6 +63,7 @@ from homeassistant.components.modbus.const import ( CONF_SWING_MODE_VALUES, CONF_VIRTUAL_COUNT, DEFAULT_SCAN_INTERVAL, + DEVICE_ID, MODBUS_DOMAIN as DOMAIN, RTUOVERTCP, SERIAL, @@ -867,7 +868,7 @@ async def test_pb_service_write( assert func_name[do_write[FUNC]].called assert func_name[do_write[FUNC]].call_args.args == (data[ATTR_ADDRESS],) assert func_name[do_write[FUNC]].call_args.kwargs == { - "slave": 17, + DEVICE_ID: 17, value_arg_name[do_write[FUNC]]: data[do_write[DATA]], } @@ -919,6 +920,9 @@ async def mock_modbus_read_pymodbus_fixture( freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60)) async_fire_time_changed(hass) await hass.async_block_till_done() + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() return mock_pymodbus @@ -1087,11 +1091,11 @@ async def test_delay( start_time = dt_util.utcnow() assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNKNOWN + assert hass.states.get(entity_id).state in (STATE_UNKNOWN, STATE_UNAVAILABLE) time_sensor_active = start_time + timedelta(seconds=2) time_after_delay = start_time + timedelta(seconds=(set_delay)) - time_after_scan = start_time + timedelta(seconds=(set_delay + set_scan_interval)) + time_after_scan = time_after_delay + timedelta(seconds=(set_scan_interval)) time_stop = time_after_scan + timedelta(seconds=10) now = start_time while now < time_stop: @@ -1104,8 +1108,13 @@ async def test_delay( await hass.async_block_till_done() if now > time_sensor_active: if now <= time_after_delay: - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - elif now > time_after_scan: + assert hass.states.get(entity_id).state in ( + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ) + if now <= time_after_delay + timedelta(seconds=2): + continue + if now > time_after_scan + timedelta(seconds=2): assert hass.states.get(entity_id).state == STATE_ON @@ -1224,6 +1233,7 @@ async def test_integration_reload( assert not state_sensor_2 +@pytest.mark.skip @pytest.mark.parametrize("do_config", [{}]) async def test_integration_reload_failed( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus @@ -1326,7 +1336,7 @@ async def test_check_default_slave( """Test default slave.""" assert mock_modbus.read_holding_registers.mock_calls first_call = mock_modbus.read_holding_registers.mock_calls[0] - assert first_call.kwargs["slave"] == expected_slave_value + assert first_call.kwargs[DEVICE_ID] == expected_slave_value @pytest.mark.parametrize( @@ -1407,7 +1417,7 @@ async def test_pb_service_write_no_slave( assert func_name[do_write[FUNC]].called assert func_name[do_write[FUNC]].call_args.args == (data[ATTR_ADDRESS],) assert func_name[do_write[FUNC]].call_args.kwargs == { - "slave": 1, + DEVICE_ID: 1, value_arg_name[do_write[FUNC]]: data[do_write[DATA]], } diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index fdaed0c323f..b3a93ec0cf2 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -316,6 +316,31 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = { }, } +MOCK_SUBENTRY_LOCK_COMPONENT = { + "3faf1318016c46c5aea26707eeb6f100": { + "platform": "lock", + "name": "Lock", + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{ value }}", + "value_template": "{{ value_json.value }}", + "code_format": "^\\d{4}$", + "payload_open": "OPEN", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_reset": "None", + "state_jammed": "JAMMED", + "state_locked": "LOCKED", + "state_locking": "LOCKING", + "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", + "retain": False, + "entity_category": None, + "entity_picture": "https://example.com/3faf1318016c46c5aea26707eeb6f100", + "optimistic": True, + }, +} + MOCK_SUBENTRY_SENSOR_COMPONENT = { "e9261f6feed443e7b7d5f3fbe2a47412": { "platform": "sensor", @@ -459,6 +484,10 @@ MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT, } +MOCK_LOCK_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_LOCK_COMPONENT, +} MOCK_SENSOR_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_SENSOR_COMPONENT, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index ff1f954bace..1c99d9da45f 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -41,6 +41,7 @@ from .common import ( MOCK_COVER_SUBENTRY_DATA_SINGLE, MOCK_FAN_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, + MOCK_LOCK_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_MULTI, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, @@ -3347,6 +3348,55 @@ async def test_migrate_of_incompatible_config_entry( ), "Milk notifier Basic light", ), + ( + MOCK_LOCK_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Lock"}, + {}, + (), + { + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "value_template": "{{ value_json.value }}", + "code_format": "^\\d{4}$", + "optimistic": True, + "retain": False, + "lock_payload_settings": { + "payload_open": "OPEN", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_reset": "None", + "state_jammed": "JAMMED", + "state_locked": "LOCKED", + "state_locking": "LOCKING", + "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", + }, + }, + ( + ( + {"command_topic": "test-topic#invalid"}, + {"command_topic": "invalid_publish_topic"}, + ), + ( + { + "command_topic": "test-topic", + "state_topic": "test-topic#invalid", + }, + {"state_topic": "invalid_subscribe_topic"}, + ), + ( + { + "command_topic": "test-topic", + "code_format": "(", + }, + {"code_format": "invalid_regular_expression"}, + ), + ), + "Milk notifier Lock", + ), + # MOCK_LOCK_SUBENTRY_DATA_SINGLE ], ids=[ "binary_sensor", @@ -3362,11 +3412,13 @@ async def test_migrate_of_incompatible_config_entry( "sensor_total", "switch", "light_basic_kelvin", + "lock", ], ) async def test_subentry_configflow( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, + mock_reload_after_entry_update: MagicMock, config_subentries_data: dict[str, Any], mock_device_user_input: dict[str, Any], mock_entity_user_input: dict[str, Any], @@ -3501,6 +3553,10 @@ async def test_subentry_configflow( assert subentry_device_data[option] == value await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + + # Assert the entry is reloaded to set up the entity + assert len(mock_reload_after_entry_update.mock_calls) == 1 @pytest.mark.parametrize( @@ -3641,6 +3697,7 @@ async def test_subentry_reconfigure_remove_entity( async def test_subentry_reconfigure_edit_entity_multi_entitites( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, + mock_reload_after_entry_update: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, user_input_mqtt: dict[str, Any], @@ -3758,6 +3815,10 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites( for key, value in user_input_mqtt.items(): assert new_components[object_list[1]][key] == value + # Assert the entry is reloaded to set up the entity + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_reload_after_entry_update.mock_calls) == 1 + @pytest.mark.parametrize( ( diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index fd54e5f0643..9d5dc8f0a8a 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -26,6 +26,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_UNIT_OF_MEASUREMENT, + UnitOfElectricPotential, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, State @@ -253,6 +254,62 @@ async def test_native_value_validation( mqtt_mock.async_publish.reset_mock() +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + number.DOMAIN: { + "name": "test", + "command_topic": "test-topic-cmd", + "state_topic": "test-topic", + "unit_of_measurement": "\u00b5V", + } + } + } + ], +) +async def test_equivalent_unit_of_measurement( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test device_class with equivalent unit of measurement.""" + assert await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", "100") + await hass.async_block_till_done() + state = hass.states.get("number.test") + assert state is not None + assert state.state == "100" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + is UnitOfElectricPotential.MICROVOLT + ) + + caplog.clear() + + discovery_payload = { + "name": "bla", + "command_topic": "test-topic2-cmd", + "state_topic": "test-topic2", + "unit_of_measurement": "\u00b5V", + } + # Now discover an invalid sensor + async_fire_mqtt_message( + hass, "homeassistant/number/bla/config", json.dumps(discovery_payload) + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic2", "21") + await hass.async_block_till_done() + state = hass.states.get("number.bla") + assert state is not None + assert state.state == "21" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + is UnitOfElectricPotential.MICROVOLT + ) + + @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 16f0c9f22bc..f7198095aa2 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -15,9 +15,11 @@ import pytest from homeassistant.components import mqtt, sensor from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, + UnitOfElectricPotential, UnitOfTemperature, ) from homeassistant.core import Event, HomeAssistant, State, callback @@ -906,6 +908,116 @@ async def test_invalid_unit_of_measurement( assert state is None +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "device_class": "voltage", + "unit_of_measurement": "\u00b5V", # microVolt + } + } + } + ], +) +async def test_device_class_with_equivalent_unit_of_measurement_received( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test device_class with equivalent unit of measurement.""" + assert await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", "100") + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state is not None + assert state.state == "100" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + is UnitOfElectricPotential.MICROVOLT + ) + + caplog.clear() + + discovery_payload = { + "name": "bla", + "state_topic": "test-topic2", + "device_class": "voltage", + "unit_of_measurement": "\u00b5V", + } + # Now discover a sensor with an altarantive mu char + async_fire_mqtt_message( + hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload) + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic2", "21") + await hass.async_block_till_done() + state = hass.states.get("sensor.bla") + assert state is not None + assert state.state == "21" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + is UnitOfElectricPotential.MICROVOLT + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "\u00b5V", + } + } + } + ], +) +async def test_equivalent_unit_of_measurement_received_without_device_class( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test device_class with equivalent unit of measurement.""" + assert await mqtt_mock_entry() + async_fire_mqtt_message(hass, "test-topic", "100") + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state is not None + assert state.state == "100" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + is UnitOfElectricPotential.MICROVOLT + ) + + caplog.clear() + + discovery_payload = { + "name": "bla", + "state_topic": "test-topic2", + "unit_of_measurement": "\u00b5V", + } + # Now discover an invalid sensor + async_fire_mqtt_message( + hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload) + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "test-topic2", "21") + await hass.async_block_till_done() + state = hass.states.get("sensor.bla") + assert state is not None + assert state.state == "21" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + is UnitOfElectricPotential.MICROVOLT + ) + + @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 77b90403823..b0c5981fbe1 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -2,6 +2,7 @@ from copy import deepcopy import json +import logging from typing import Any from unittest.mock import patch @@ -395,6 +396,15 @@ async def test_status_with_deprecated_battery_feature( assert issue.issue_domain == "vacuum" assert issue.translation_key == "deprecated_vacuum_battery_feature" assert issue.translation_placeholders == {"entity_id": "vacuum.mqtttest"} + assert not [ + record + for record in caplog.records + if record.name == "homeassistant.helpers.frame" + and record.levelno >= logging.WARNING + ] + assert ( + "mqtt' is setting the battery_level which has been deprecated" + ) not in caplog.text @pytest.mark.parametrize( diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr index cc6bc9bc7b6..3071752267e 100644 --- a/tests/components/nam/snapshots/test_sensor.ambr +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -981,7 +981,7 @@ 'supported_features': 0, 'translation_key': 'pmsx003_pm1', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-state] @@ -990,7 +990,7 @@ 'device_class': 'pm1', 'friendly_name': 'Nettigo Air Monitor PMSx003 PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm1', @@ -1037,7 +1037,7 @@ 'supported_features': 0, 'translation_key': 'pmsx003_pm10', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p1', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-state] @@ -1046,7 +1046,7 @@ 'device_class': 'pm10', 'friendly_name': 'Nettigo Air Monitor PMSx003 PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm10', @@ -1093,7 +1093,7 @@ 'supported_features': 0, 'translation_key': 'pmsx003_pm25', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-state] @@ -1102,7 +1102,7 @@ 'device_class': 'pm25', 'friendly_name': 'Nettigo Air Monitor PMSx003 PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm2_5', @@ -1261,7 +1261,7 @@ 'supported_features': 0, 'translation_key': 'sds011_pm10', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p1', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-state] @@ -1270,7 +1270,7 @@ 'device_class': 'pm10', 'friendly_name': 'Nettigo Air Monitor SDS011 PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm10', @@ -1317,7 +1317,7 @@ 'supported_features': 0, 'translation_key': 'sds011_pm25', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-state] @@ -1326,7 +1326,7 @@ 'device_class': 'pm25', 'friendly_name': 'Nettigo Air Monitor SDS011 PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm2_5', @@ -1653,7 +1653,7 @@ 'supported_features': 0, 'translation_key': 'sps30_pm1', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p0', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-state] @@ -1662,7 +1662,7 @@ 'device_class': 'pm1', 'friendly_name': 'Nettigo Air Monitor SPS30 PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm1', @@ -1709,7 +1709,7 @@ 'supported_features': 0, 'translation_key': 'sps30_pm10', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p1', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-state] @@ -1718,7 +1718,7 @@ 'device_class': 'pm10', 'friendly_name': 'Nettigo Air Monitor SPS30 PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm10', @@ -1765,7 +1765,7 @@ 'supported_features': 0, 'translation_key': 'sps30_pm25', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-state] @@ -1774,7 +1774,7 @@ 'device_class': 'pm25', 'friendly_name': 'Nettigo Air Monitor SPS30 PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm2_5', @@ -1821,7 +1821,7 @@ 'supported_features': 0, 'translation_key': 'sps30_pm4', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p4', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-state] @@ -1829,7 +1829,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Nettigo Air Monitor SPS30 PM4', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4', diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index ba89405bc97..d9616572b2e 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -10,6 +10,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.nanoleaf.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -463,3 +464,59 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_abort_discovery_flow_with_user_flow(hass: HomeAssistant) -> None: + """Test abort discovery flow if user flow is already in progress.""" + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.load_json_object", + return_value={}, + ), + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), + ), + patch( + "homeassistant.components.nanoleaf.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + upnp={}, + ssdp_headers={ + "_host": TEST_HOST, + "nl-devicename": TEST_NAME, + "nl-deviceid": TEST_DEVICE_ID, + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] is None + assert result["step_id"] == "link" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "link" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # Verify the discovery flow was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 4cf74d72e63..ef46eecaa66 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -13,8 +13,6 @@ from nextdns import ( Settings, ) -from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN -from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -39,6 +37,7 @@ SETTINGS = Settings( ai_threat_detection=True, allow_affiliate=True, anonymized_ecs=True, + bav=True, block_bypass_methods=True, block_csam=True, block_ddns=True, @@ -154,20 +153,12 @@ def mock_nextdns(): yield -async def init_integration(hass: HomeAssistant) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Set up the NextDNS integration in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Fake Profile", - unique_id="xyz12", - data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, - entry_id="d9aa37407ddac7b964a99e86312288d6", - ) - - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) with mock_nextdns(): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - - return entry diff --git a/tests/components/nextdns/conftest.py b/tests/components/nextdns/conftest.py new file mode 100644 index 00000000000..b46c51d673c --- /dev/null +++ b/tests/components/nextdns/conftest.py @@ -0,0 +1,32 @@ +"""Common fixtures for the NextDNS tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN +from homeassistant.const import CONF_API_KEY + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.nextdns.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + entry_id="d9aa37407ddac7b964a99e86312288d6", + ) diff --git a/tests/components/nextdns/snapshots/test_diagnostics.ambr b/tests/components/nextdns/snapshots/test_diagnostics.ambr index 23f42fee077..f55c381af4e 100644 --- a/tests/components/nextdns/snapshots/test_diagnostics.ambr +++ b/tests/components/nextdns/snapshots/test_diagnostics.ambr @@ -56,6 +56,7 @@ 'ai_threat_detection': True, 'allow_affiliate': True, 'anonymized_ecs': True, + 'bav': True, 'block_9gag': True, 'block_amazon': True, 'block_bereal': True, diff --git a/tests/components/nextdns/snapshots/test_switch.ambr b/tests/components/nextdns/snapshots/test_switch.ambr index 0b25baecd20..d2a78a61127 100644 --- a/tests/components/nextdns/snapshots/test_switch.ambr +++ b/tests/components/nextdns/snapshots/test_switch.ambr @@ -2879,6 +2879,54 @@ 'state': 'on', }) # --- +# name: test_switch[switch.fake_profile_bypass_age_verification-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.fake_profile_bypass_age_verification', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass age verification', + 'platform': 'nextdns', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass_age_verification', + 'unique_id': 'xyz12_bav', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.fake_profile_bypass_age_verification-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fake Profile Bypass age verification', + }), + 'context': , + 'entity_id': 'switch.fake_profile_bypass_age_verification', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switch[switch.fake_profile_cache_boost-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py index 99e40af0dce..c9ad0d6e209 100644 --- a/tests/components/nextdns/test_binary_sensor.py +++ b/tests/components/nextdns/test_binary_sensor.py @@ -3,56 +3,65 @@ from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from nextdns import ApiError from syrupy.assertion import SnapshotAssertion -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_binary_sensor( - hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test states of the binary sensors.""" with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BINARY_SENSOR]): - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_availability(hass: HomeAssistant) -> None: +async def test_availability( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" - await init_integration(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BINARY_SENSOR]): + await init_integration(hass, mock_config_entry) - state = hass.states.get("binary_sensor.fake_profile_device_connection_status") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == STATE_ON + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + entity_ids = (entry.entity_id for entry in entity_entries) - future = utcnow() + timedelta(minutes=10) + for entity_id in entity_ids: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + freezer.tick(timedelta(minutes=10)) with patch( "homeassistant.components.nextdns.NextDns.connection_status", side_effect=ApiError("API Error"), ): - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("binary_sensor.fake_profile_device_connection_status") - assert state - assert state.state == STATE_UNAVAILABLE + for entity_id in entity_ids: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - future = utcnow() + timedelta(minutes=20) + freezer.tick(timedelta(minutes=10)) with mock_nextdns(): - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("binary_sensor.fake_profile_device_connection_status") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == STATE_ON + for entity_id in entity_ids: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index 0cb4a7cd0df..03108e81984 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -15,31 +15,34 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from . import init_integration -from tests.common import snapshot_platform +from tests.common import MockConfigEntry, snapshot_platform async def test_button( - hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test states of the button.""" with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.BUTTON]): - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_button_press(hass: HomeAssistant) -> None: +@pytest.mark.freeze_time("2023-10-21") +async def test_button_press( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test button press.""" - await init_integration(hass) + await init_integration(hass, mock_config_entry) - now = dt_util.utcnow() with ( patch("homeassistant.components.nextdns.NextDns.clear_logs") as mock_clear_logs, - patch("homeassistant.core.dt_util.utcnow", return_value=now), ): await hass.services.async_call( BUTTON_DOMAIN, @@ -53,7 +56,7 @@ async def test_button_press(hass: HomeAssistant) -> None: state = hass.states.get("button.fake_profile_clear_logs") assert state - assert state.state == now.isoformat() + assert state.state == "2023-10-21T00:00:00+00:00" @pytest.mark.parametrize( @@ -65,9 +68,11 @@ async def test_button_press(hass: HomeAssistant) -> None: ClientError, ], ) -async def test_button_failure(hass: HomeAssistant, exc: Exception) -> None: +async def test_button_failure( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, exc: Exception +) -> None: """Tests that the press action throws HomeAssistantError.""" - await init_integration(hass) + await init_integration(hass, mock_config_entry) with ( patch("homeassistant.components.nextdns.NextDns.clear_logs", side_effect=exc), @@ -84,9 +89,11 @@ async def test_button_failure(hass: HomeAssistant, exc: Exception) -> None: ) -async def test_button_auth_error(hass: HomeAssistant) -> None: +async def test_button_auth_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Tests that the press action starts re-auth flow.""" - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) with patch( "homeassistant.components.nextdns.NextDns.clear_logs", @@ -99,7 +106,7 @@ async def test_button_auth_error(hass: HomeAssistant) -> None: blocking=True, ) - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -110,4 +117,4 @@ async def test_button_auth_error(hass: HomeAssistant) -> None: assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH - assert flow["context"].get("entry_id") == entry.entry_id + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 27a6cf1e7e0..d577fb21845 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -1,6 +1,6 @@ """Define tests for the NextDNS config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from nextdns import ApiError, InvalidApiKeyError import pytest @@ -14,8 +14,12 @@ from homeassistant.data_entry_flow import FlowResultType from . import PROFILES, init_integration, mock_nextdns +from tests.common import MockConfigEntry -async def test_form_create_entry(hass: HomeAssistant) -> None: + +async def test_form_create_entry( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test that the user step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -24,14 +28,9 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with ( - patch( - "homeassistant.components.nextdns.NextDns.get_profiles", - return_value=PROFILES, - ), - patch( - "homeassistant.components.nextdns.async_setup_entry", return_value=True - ) as mock_setup_entry, + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", + return_value=PROFILES, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -44,12 +43,12 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Fake Profile" assert result["data"][CONF_API_KEY] == "fake_api_key" assert result["data"][CONF_PROFILE_ID] == "xyz12" + assert result["result"].unique_id == "xyz12" assert len(mock_setup_entry.mock_calls) == 1 @@ -64,24 +63,55 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ], ) async def test_form_errors( - hass: HomeAssistant, exc: Exception, base_error: str + hass: HomeAssistant, mock_setup_entry: AsyncMock, exc: Exception, base_error: str ) -> None: """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + with patch( "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=exc ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_API_KEY: "fake_api_key"}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, ) + assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": base_error} + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", + return_value=PROFILES, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, + ) -async def test_form_already_configured(hass: HomeAssistant) -> None: + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "profiles" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Fake Profile" + assert result["data"][CONF_API_KEY] == "fake_api_key" + assert result["data"][CONF_PROFILE_ID] == "xyz12" + assert result["result"].unique_id == "xyz12" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test that errors are shown when duplicates are added.""" - await init_integration(hass) + await init_integration(hass, mock_config_entry) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -103,11 +133,13 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_reauth_successful(hass: HomeAssistant) -> None: +async def test_reauth_successful( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test starting a reauthentication flow.""" - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - result = await entry.start_reauth_flow(hass) + result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -122,7 +154,6 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_API_KEY: "new_api_key"}, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -139,12 +170,15 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: ], ) async def test_reauth_errors( - hass: HomeAssistant, exc: Exception, base_error: str + hass: HomeAssistant, + exc: Exception, + base_error: str, + mock_config_entry: MockConfigEntry, ) -> None: """Test reauthentication flow with errors.""" - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - result = await entry.start_reauth_flow(hass) + result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -155,6 +189,20 @@ async def test_reauth_errors( result["flow_id"], user_input={CONF_API_KEY: "new_api_key"}, ) - await hass.async_block_till_done() assert result["errors"] == {"base": base_error} + + with ( + patch( + "homeassistant.components.nextdns.NextDns.get_profiles", + return_value=PROFILES, + ), + mock_nextdns(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_KEY: "new_api_key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/nextdns/test_coordinator.py b/tests/components/nextdns/test_coordinator.py index f2b353ea2c5..83748f836b5 100644 --- a/tests/components/nextdns/test_coordinator.py +++ b/tests/components/nextdns/test_coordinator.py @@ -12,17 +12,18 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed async def test_auth_error( hass: HomeAssistant, freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, ) -> None: """Test authentication error when polling data.""" - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED freezer.tick(timedelta(minutes=10)) with ( @@ -62,7 +63,7 @@ async def test_auth_error( async_fire_time_changed(hass) await hass.async_block_till_done() - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -73,4 +74,4 @@ async def test_auth_error( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH - assert flow["context"].get("entry_id") == entry.entry_id + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 4a5e09908ec..2b0c0564649 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant from . import init_integration +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -15,10 +16,11 @@ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test config entry diagnostics.""" - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( - exclude=props("created_at", "modified_at") - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index 0a0bf3fc487..217e75ca701 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -6,9 +6,9 @@ from nextdns import ApiError, InvalidApiKeyError import pytest from tenacity import RetryError -from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN +from homeassistant.components.nextdns.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_API_KEY, STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from . import init_integration @@ -16,9 +16,11 @@ from . import init_integration from tests.common import MockConfigEntry -async def test_async_setup_entry(hass: HomeAssistant) -> None: +async def test_async_setup_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test a successful setup entry.""" - await init_integration(hass) + await init_integration(hass, mock_config_entry) state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") assert state is not None @@ -29,55 +31,48 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "exc", [ApiError("API Error"), RetryError("Retry Error"), TimeoutError] ) -async def test_config_not_ready(hass: HomeAssistant, exc: Exception) -> None: +async def test_config_not_ready( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, exc: Exception +) -> None: """Test for setup failure if the connection to the service fails.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Fake Profile", - unique_id="xyz12", - data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, - ) - with patch( "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=exc, ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_RETRY + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass: HomeAssistant) -> None: +async def test_unload_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test successful unload of entry.""" - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(entry.entry_id) + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert entry.state is ConfigEntryState.NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) -async def test_config_auth_failed(hass: HomeAssistant) -> None: +async def test_config_auth_failed( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test for setup failure if the auth fails.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Fake Profile", - unique_id="xyz12", - data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) with patch( "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=InvalidApiKeyError, ): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -88,4 +83,4 @@ async def test_config_auth_failed(hass: HomeAssistant) -> None: assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH - assert flow["context"].get("entry_id") == entry.entry_id + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index 43e823fbf38..3ef1ab55f9f 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from nextdns import ApiError import pytest from syrupy.assertion import SnapshotAssertion @@ -10,11 +11,10 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -22,48 +22,35 @@ async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test states of sensors.""" with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SENSOR]): - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_availability( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, ) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" - await init_integration(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SENSOR]): + await init_integration(hass, mock_config_entry) - state = hass.states.get("sensor.fake_profile_dns_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "100" + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + entity_ids = (entry.entity_id for entry in entity_entries) - state = hass.states.get("sensor.fake_profile_dns_over_https_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "20" + for entity_id in entity_ids: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE - state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "75" - - state = hass.states.get("sensor.fake_profile_encrypted_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "60" - - state = hass.states.get("sensor.fake_profile_ipv4_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "90" - - future = utcnow() + timedelta(minutes=10) + freezer.tick(timedelta(minutes=10)) with ( patch( "homeassistant.components.nextdns.NextDns.get_analytics_status", @@ -86,55 +73,16 @@ async def test_availability( side_effect=ApiError("API Error"), ), ): - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("sensor.fake_profile_dns_queries") - assert state - assert state.state == STATE_UNAVAILABLE + for entity_id in entity_ids: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - state = hass.states.get("sensor.fake_profile_dns_over_https_queries") - assert state - assert state.state == STATE_UNAVAILABLE - - state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") - assert state - assert state.state == STATE_UNAVAILABLE - - state = hass.states.get("sensor.fake_profile_encrypted_queries") - assert state - assert state.state == STATE_UNAVAILABLE - - state = hass.states.get("sensor.fake_profile_ipv4_queries") - assert state - assert state.state == STATE_UNAVAILABLE - - future = utcnow() + timedelta(minutes=20) + freezer.tick(timedelta(minutes=10)) with mock_nextdns(): - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("sensor.fake_profile_dns_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "100" - - state = hass.states.get("sensor.fake_profile_dns_over_https_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "20" - - state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "75" - - state = hass.states.get("sensor.fake_profile_encrypted_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "60" - - state = hass.states.get("sensor.fake_profile_ipv4_queries") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "90" + for entity_id in entity_ids: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 1b0edb2c83c..645ca11ac49 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError +from freezegun.api import FrozenDateTimeFactory from nextdns import ApiError, InvalidApiKeyError import pytest from syrupy.assertion import SnapshotAssertion @@ -25,11 +26,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow from . import init_integration, mock_nextdns -from tests.common import async_fire_time_changed, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -37,17 +37,20 @@ async def test_switch( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test states of the switches.""" with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SWITCH]): - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_switch_on(hass: HomeAssistant) -> None: +async def test_switch_on( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test the switch can be turned on.""" - await init_integration(hass) + await init_integration(hass, mock_config_entry) state = hass.states.get("switch.fake_profile_block_page") assert state @@ -71,9 +74,11 @@ async def test_switch_on(hass: HomeAssistant) -> None: mock_switch_on.assert_called_once() -async def test_switch_off(hass: HomeAssistant) -> None: +async def test_switch_off( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test the switch can be turned on.""" - await init_integration(hass) + await init_integration(hass, mock_config_entry) state = hass.states.get("switch.fake_profile_web3") assert state @@ -97,6 +102,7 @@ async def test_switch_off(hass: HomeAssistant) -> None: mock_switch_on.assert_called_once() +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( "exc", [ @@ -105,36 +111,43 @@ async def test_switch_off(hass: HomeAssistant) -> None: TimeoutError, ], ) -async def test_availability(hass: HomeAssistant, exc: Exception) -> None: +async def test_availability( + hass: HomeAssistant, + exc: Exception, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" - await init_integration(hass) + with patch("homeassistant.components.nextdns.PLATFORMS", [Platform.SWITCH]): + await init_integration(hass, mock_config_entry) - state = hass.states.get("switch.fake_profile_web3") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == STATE_ON + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + entity_ids = (entry.entity_id for entry in entity_entries) - future = utcnow() + timedelta(minutes=10) + for entity_id in entity_ids: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + freezer.tick(timedelta(minutes=10)) with patch( "homeassistant.components.nextdns.NextDns.get_settings", side_effect=exc, ): - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("switch.fake_profile_web3") - assert state - assert state.state == STATE_UNAVAILABLE + for entity_id in entity_ids: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - future = utcnow() + timedelta(minutes=20) + freezer.tick(timedelta(minutes=10)) with mock_nextdns(): - async_fire_time_changed(hass, future) + async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("switch.fake_profile_web3") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == STATE_ON + for entity_id in entity_ids: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE @pytest.mark.parametrize( @@ -146,9 +159,11 @@ async def test_availability(hass: HomeAssistant, exc: Exception) -> None: ClientError, ], ) -async def test_switch_failure(hass: HomeAssistant, exc: Exception) -> None: +async def test_switch_failure( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, exc: Exception +) -> None: """Tests that the turn on/off service throws HomeAssistantError.""" - await init_integration(hass) + await init_integration(hass, mock_config_entry) with ( patch("homeassistant.components.nextdns.NextDns.set_setting", side_effect=exc), @@ -162,9 +177,11 @@ async def test_switch_failure(hass: HomeAssistant, exc: Exception) -> None: ) -async def test_switch_auth_error(hass: HomeAssistant) -> None: +async def test_switch_auth_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Tests that the turn on/off action starts re-auth flow.""" - entry = await init_integration(hass) + await init_integration(hass, mock_config_entry) with patch( "homeassistant.components.nextdns.NextDns.set_setting", @@ -177,7 +194,7 @@ async def test_switch_auth_error(hass: HomeAssistant) -> None: blocking=True, ) - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -188,4 +205,4 @@ async def test_switch_auth_error(hass: HomeAssistant) -> None: assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH - assert flow["context"].get("entry_id") == entry.entry_id + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/nina/snapshots/test_diagnostics.ambr b/tests/components/nina/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..aaf42471912 --- /dev/null +++ b/tests/components/nina/snapshots/test_diagnostics.ambr @@ -0,0 +1,45 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + '083350000000': list([ + dict({ + 'affected_areas': 'Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere.', + 'description': 'Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.', + 'expires': '3021-11-22T05:19:00+01:00', + 'headline': 'Ausfall Notruf 112', + 'id': 'mow.DE-NW-BN-SE030-20201014-30-000', + 'is_valid': True, + 'recommended_actions': '', + 'sender': 'Deutscher Wetterdienst', + 'sent': '2021-10-11T05:20:00+01:00', + 'severity': 'Minor', + 'start': '2021-11-01T05:20:00+01:00', + 'web': 'https://www.wettergefahren.de', + }), + dict({ + 'affected_areas': 'Axstedt, Gnarrenburg, Grasberg, Hagen im Bremischen, Hambergen, Hepstedt, Holste, Lilienthal, Lübberstedt, Osterholz-Scharmbeck, Ritterhude, Schwanewede, Vollersode, Worpswede', + 'description': 'In Beverstedt im Landkreis Cuxhaven ist am 20. Juli 2022 in einer Geflügelhaltung der Ausbruch der Geflügelpest (Vogelgrippe, Aviäre Influenza) amtlich festgestellt worden. Durch die geografische Nähe des Ausbruchsbetriebes zum Gebiet des Landkreises Osterholz musste das Veterinäramt des Landkreises zum Schutz vor einer Ausbreitung der Geflügelpest auch für sein Gebiet ein Restriktionsgebiet festlegen. Rund um den Ausbruchsort wurde eine Überwachungszone ausgewiesen. Eine entsprechende Tierseuchenbehördliche Allgemeinverfügung wurde vom Landkreis Osterholz erlassen und tritt am 23.07.2022 in Kraft.
\xa0
Die Überwachungszone mit einem Radius von mindestens zehn Kilometern um den Ausbruchsbetrieb erstreckt sich im Landkreis Osterholz innerhalb der Samtgemeinde Hambergen auf die Mitgliedsgemeinden Axstedt, Holste und Lübberstedt. Die vorgenannten Gemeinden sind vollständig zur Überwachungszone erklärt worden. Der genaue Grenzverlauf des Gebietes kann auch der interaktiven Karte im Internet entnommen werden.
\xa0
In der Überwachungszone liegen im Landkreis Osterholz rund 70 Geflügelhaltungen mit einem Gesamtbestand von rund 1.800 Tieren. Sie alle unterliegen mit der Allgemeinverfügung der sogenannten amtlichen Beobachtung. Für die Betriebe sind die Biosicherheitsmaßnahmen einzuhalten. Dazu zählen insbesondere Hygienemaßnahmen im laufenden Betrieb und eine ordnungsgemäße Schadnagerbekämpfung.
\xa0
Das Verbringen von Vögeln, Fleisch von Geflügel, Eiern und sonstige Nebenprodukte von Geflügel in und aus Betrieben in der Überwachungszone ist verboten. Auch Geflügeltransporte sind in der Überwachungszone verboten. Jeder Verdacht der Erkrankung auf Geflügelpest ist zudem dem Veterinäramt des Landkreises Osterholz unter der E-Mail-Adresse veterinaeramt@landkreis-osterholz.de sofort zu melden. Alle Hinweise, die innerhalb der Überwachungszone zu beachten sind, sind unter www.landkreis-osterholz.de/gefluegelpest zusammengefasst dargestellt.
\xa0
Die Veterinärbehörde weist zudem darauf hin, dass sämtliche Geflügelhaltungen – Hühner, Enten, Gänse, Fasane, Perlhühner, Rebhühner, Truthühner, Wachteln oder Laufvögel – der zuständigen Behörde angezeigt werden müssen. Wer dies bisher noch nicht gemacht hat und über keine Registriernummer für seinen Geflügelbestand verfügt, sollte die Meldung über das Veterinäramt umgehend nachholen.
\xa0
Das Beobachtungsgebiet kann frühestens 30 Tage nach der Grobreinigung des Ausbruchsbetriebes wieder aufgehoben werden. Hierüber wird der Landkreis Osterholz informieren.
\xa0
Die Allgemeinverfügung, eine Übersicht zur Überwachungszone und weitere Hinweise sind auf der Internetseite unter www.landkreis-osterholz.de/gefluegelpest zu finden.', + 'expires': '2002-08-07T10:59:00+02:00', + 'headline': 'Geflügelpest im Landkreis Cuxhaven - Teile des Landkreises Osterholz zur Überwachungszone erklärt', + 'id': 'biw.BIWAPP-69634', + 'is_valid': False, + 'recommended_actions': '', + 'sender': '', + 'sent': '1999-08-07T10:59:00+02:00', + 'severity': 'Minor', + 'start': '', + 'web': '', + }), + ]), + }), + 'entry_data': dict({ + 'area_filter': '.*', + 'headline_filter': '.*corona.*', + 'regions': dict({ + '083350000000': 'Aach, Stadt', + }), + 'slots': 5, + }), + }) +# --- diff --git a/tests/components/nina/test_diagnostics.py b/tests/components/nina/test_diagnostics.py new file mode 100644 index 00000000000..c0646b8d68c --- /dev/null +++ b/tests/components/nina/test_diagnostics.py @@ -0,0 +1,45 @@ +"""Test the Nina diagnostics.""" + +from typing import Any +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.nina.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import mocked_request_function + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + +ENTRY_DATA: dict[str, Any] = { + "slots": 5, + "corona_filter": True, + "regions": {"083350000000": "Aach, Stadt"}, +} + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ): + config_entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA + ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 4ccf8f69c42..b5e5e18f664 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant.components.number import ( + AMBIGUOUS_UNITS, ATTR_MAX, ATTR_MIN, ATTR_MODE, @@ -48,6 +49,7 @@ from . import common from tests.common import ( MockConfigEntry, + MockEntity, MockModule, MockPlatform, async_mock_restore_state_shutdown_restart, @@ -61,6 +63,25 @@ from tests.common import ( TEST_DOMAIN = "test" +class MockNumber(MockEntity, NumberEntity): + """Mock NumberEntity class to test unit of measurement.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") + + @property + def native_unit_of_measurement(self): + """Return the native unit_of_measurement of this sensor.""" + return self._handle("native_unit_of_measurement") + + @property + def native_value(self): + """Return the native value of this sensor.""" + return self._handle("native_value") + + class MockDefaultNumberEntity(NumberEntity): """Mock NumberEntity device to use in tests. @@ -900,6 +921,33 @@ async def test_translated_unit_with_native_unit_raises( assert entity0.entity_id is None +@pytest.mark.parametrize( + ("ambiguous_unit", "normalized_unit"), + [ + (ambiguous_unit, normalized_unit) + for ambiguous_unit, normalized_unit in AMBIGUOUS_UNITS.items() + ], +) +async def test_ambiguous_unit_of_measurement_compat( + hass: HomeAssistant, ambiguous_unit: str, normalized_unit: str +) -> None: + """Test ambiguous native_unit_of_measurement values are corrected.""" + entity0 = MockNumber( + name="Test", + native_value="0.0", + native_unit_of_measurement=ambiguous_unit, + ) + setup_test_component_platform(hass, DOMAIN, [entity0]) + + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + # Check compatible unit is applied + state = hass.states.get(entity0.entity_id) + assert state.state == "0.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == normalized_unit + + def test_device_classes_aligned() -> None: """Make sure all sensor device classes are also available in NumberDeviceClass.""" diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index 6ea2ad11103..c3d8d92a9d2 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -25,7 +25,7 @@ 'name': 'Pool 1', 'name_by_user': None, 'primary_config_entry': , - 'serial_number': None, + 'serial_number': 'W1122333044455', 'sw_version': '1.7.1-stable', 'via_device_id': None, }) @@ -56,7 +56,7 @@ 'name': 'Pool 2', 'name_by_user': None, 'primary_config_entry': , - 'serial_number': None, + 'serial_number': 'W2233304445566', 'sw_version': '1.7.1-stable', 'via_device_id': None, }) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 370bcc871c6..32804bca28e 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -15,6 +15,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_INJECT_READS: { "/type": [b"DS2405"], "/PIO": [b" 1"], + "/sensed": [b" 1"], }, }, "10.111111111111": { diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index bce1251904a..521e5c50925 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -1,4 +1,53 @@ # serializer version: 1 +# name: test_binary_sensors[binary_sensor.05_111111111111_sensed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.05_111111111111_sensed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed', + 'platform': 'onewire', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sensed', + 'unique_id': '/05.111111111111/sensed', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.05_111111111111_sensed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/05.111111111111/sensed', + 'friendly_name': '05.111111111111 Sensed', + }), + 'context': , + 'entity_id': 'binary_sensor.05_111111111111_sensed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensors[binary_sensor.12_111111111111_sensed_a-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/open_router/snapshots/test_conversation.ambr b/tests/components/open_router/snapshots/test_conversation.ambr index d119c2f6aa5..19b5785a9eb 100644 --- a/tests/components/open_router/snapshots/test_conversation.ambr +++ b/tests/components/open_router/snapshots/test_conversation.ambr @@ -113,7 +113,9 @@ dict({ 'agent_id': 'conversation.gpt_3_5_turbo', 'content': 'Hello, how can I help you?', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) @@ -128,9 +130,12 @@ dict({ 'agent_id': 'conversation.gpt_3_5_turbo', 'content': None, + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'call_call_1', 'tool_args': dict({ 'param1': 'call1', @@ -149,7 +154,9 @@ dict({ 'agent_id': 'conversation.gpt_3_5_turbo', 'content': 'I have successfully called the function', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) diff --git a/tests/components/open_router/test_conversation.py b/tests/components/open_router/test_conversation.py index afbdd907f93..edd47572120 100644 --- a/tests/components/open_router/test_conversation.py +++ b/tests/components/open_router/test_conversation.py @@ -7,10 +7,10 @@ from openai.types import CompletionUsage from openai.types.chat import ( ChatCompletion, ChatCompletionMessage, - ChatCompletionMessageToolCall, + ChatCompletionMessageFunctionToolCall, ) from openai.types.chat.chat_completion import Choice -from openai.types.chat.chat_completion_message_tool_call import Function +from openai.types.chat.chat_completion_message_function_tool_call_param import Function import pytest from syrupy.assertion import SnapshotAssertion @@ -133,7 +133,7 @@ async def test_function_call( role="assistant", function_call=None, tool_calls=[ - ChatCompletionMessageToolCall( + ChatCompletionMessageFunctionToolCall( id="call_call_1", function=Function( arguments='{"param1":"call1"}', diff --git a/tests/components/openai_conversation/__init__.py b/tests/components/openai_conversation/__init__.py index c10c23df237..e8effca3bc5 100644 --- a/tests/components/openai_conversation/__init__.py +++ b/tests/components/openai_conversation/__init__.py @@ -18,6 +18,10 @@ from openai.types.responses import ( ResponseOutputMessage, ResponseOutputText, ResponseReasoningItem, + ResponseReasoningSummaryPartAddedEvent, + ResponseReasoningSummaryPartDoneEvent, + ResponseReasoningSummaryTextDeltaEvent, + ResponseReasoningSummaryTextDoneEvent, ResponseStreamEvent, ResponseTextDeltaEvent, ResponseTextDoneEvent, @@ -25,7 +29,9 @@ from openai.types.responses import ( ResponseWebSearchCallInProgressEvent, ResponseWebSearchCallSearchingEvent, ) +from openai.types.responses.response_code_interpreter_tool_call import OutputLogs from openai.types.responses.response_function_web_search import ActionSearch +from openai.types.responses.response_reasoning_item import Summary def create_message_item( @@ -65,6 +71,7 @@ def create_message_item( content_index=0, delta=delta, item_id=id, + logprobs=[], output_index=output_index, sequence_number=0, type="response.output_text.delta", @@ -77,6 +84,7 @@ def create_message_item( ResponseTextDoneEvent( content_index=0, item_id=id, + logprobs=[], output_index=output_index, text="".join(text), sequence_number=0, @@ -171,9 +179,23 @@ def create_function_tool_call_item( return events -def create_reasoning_item(id: str, output_index: int) -> list[ResponseStreamEvent]: +def create_reasoning_item( + id: str, + output_index: int, + reasoning_summary: list[list[str]] | list[str] | str | None = None, +) -> list[ResponseStreamEvent]: """Create a reasoning item.""" - return [ + + if reasoning_summary is None: + reasoning_summary = [[]] + elif isinstance(reasoning_summary, str): + reasoning_summary = [reasoning_summary] + if isinstance(reasoning_summary, list) and all( + isinstance(item, str) for item in reasoning_summary + ): + reasoning_summary = [reasoning_summary] + + events = [ ResponseOutputItemAddedEvent( item=ResponseReasoningItem( id=id, @@ -185,11 +207,60 @@ def create_reasoning_item(id: str, output_index: int) -> list[ResponseStreamEven output_index=output_index, sequence_number=0, type="response.output_item.added", - ), + ) + ] + + for summary_index, summary in enumerate(reasoning_summary): + events.append( + ResponseReasoningSummaryPartAddedEvent( + item_id=id, + output_index=output_index, + part={"text": "", "type": "summary_text"}, + sequence_number=0, + summary_index=summary_index, + type="response.reasoning_summary_part.added", + ) + ) + events.extend( + ResponseReasoningSummaryTextDeltaEvent( + delta=delta, + item_id=id, + output_index=output_index, + sequence_number=0, + summary_index=summary_index, + type="response.reasoning_summary_text.delta", + ) + for delta in summary + ) + events.extend( + [ + ResponseReasoningSummaryTextDoneEvent( + item_id=id, + output_index=output_index, + sequence_number=0, + summary_index=summary_index, + text="".join(summary), + type="response.reasoning_summary_text.done", + ), + ResponseReasoningSummaryPartDoneEvent( + item_id=id, + output_index=output_index, + part={"text": "".join(summary), "type": "summary_text"}, + sequence_number=0, + summary_index=summary_index, + type="response.reasoning_summary_part.done", + ), + ] + ) + + events.append( ResponseOutputItemDoneEvent( item=ResponseReasoningItem( id=id, - summary=[], + summary=[ + Summary(text="".join(summary), type="summary_text") + for summary in reasoning_summary + ], type="reasoning", status=None, encrypted_content="AAABBB", @@ -198,7 +269,9 @@ def create_reasoning_item(id: str, output_index: int) -> list[ResponseStreamEven sequence_number=0, type="response.output_item.done", ), - ] + ) + + return events def create_web_search_item(id: str, output_index: int) -> list[ResponseStreamEvent]: @@ -248,7 +321,7 @@ def create_web_search_item(id: str, output_index: int) -> list[ResponseStreamEve def create_code_interpreter_item( - id: str, code: str | list[str], output_index: int + id: str, code: str | list[str], output_index: int, logs: str | None = None ) -> list[ResponseStreamEvent]: """Create a message item.""" if isinstance(code, str): @@ -316,7 +389,7 @@ def create_code_interpreter_item( id=id, code=code, container_id=container_id, - outputs=None, + outputs=[OutputLogs(type="logs", logs=logs)] if logs else None, status="completed", type="code_interpreter_call", ), diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index b58e6c31f38..38d8967e6c5 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -94,7 +94,7 @@ def mock_config_entry_with_reasoning_model( hass.config_entries.async_update_subentry( mock_config_entry, next(iter(mock_config_entry.subentries.values())), - data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "o4-mini"}, + data={CONF_LLM_HASS_API: llm.LLM_API_ASSIST, CONF_CHAT_MODEL: "gpt-5-mini"}, ) return mock_config_entry diff --git a/tests/components/openai_conversation/snapshots/test_conversation.ambr b/tests/components/openai_conversation/snapshots/test_conversation.ambr index 77c52ab97e6..473d32a53f8 100644 --- a/tests/components/openai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/openai_conversation/snapshots/test_conversation.ambr @@ -1,4 +1,39 @@ # serializer version: 1 +# name: test_code_interpreter + list([ + dict({ + 'content': 'Please use the python tool to calculate square root of 55555', + 'role': 'user', + 'type': 'message', + }), + dict({ + 'arguments': '{"code": "import math\\nmath.sqrt(55555)", "container": "cntr_A"}', + 'call_id': 'ci_A', + 'name': 'code_interpreter', + 'type': 'function_call', + }), + dict({ + 'call_id': 'ci_A', + 'output': '{"output": [{"logs": "235.70108188126758\\n", "type": "logs"}]}', + 'type': 'function_call_output', + }), + dict({ + 'content': 'I’ve calculated it with Python: the square root of 55555 is approximately 235.70108188126758.', + 'role': 'assistant', + 'type': 'message', + }), + dict({ + 'content': 'Thank you!', + 'role': 'user', + 'type': 'message', + }), + dict({ + 'content': 'You are welcome!', + 'role': 'assistant', + 'type': 'message', + }), + ]) +# --- # name: test_function_call list([ dict({ @@ -9,9 +44,28 @@ dict({ 'agent_id': 'conversation.openai_conversation', 'content': None, + 'native': None, 'role': 'assistant', + 'thinking_content': 'Thinking', + 'tool_calls': None, + }), + dict({ + 'agent_id': 'conversation.openai_conversation', + 'content': None, + 'native': ResponseReasoningItem(id='rs_A', summary=[], type='reasoning', content=None, encrypted_content='AAABBB', status=None), + 'role': 'assistant', + 'thinking_content': 'Thinking more', + 'tool_calls': None, + }), + dict({ + 'agent_id': 'conversation.openai_conversation', + 'content': None, + 'native': None, + 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'call_call_1', 'tool_args': dict({ 'param1': 'call1', @@ -30,9 +84,12 @@ dict({ 'agent_id': 'conversation.openai_conversation', 'content': None, + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'call_call_2', 'tool_args': dict({ 'param1': 'call2', @@ -51,11 +108,64 @@ dict({ 'agent_id': 'conversation.openai_conversation', 'content': 'Cool', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) # --- +# name: test_function_call.1 + list([ + dict({ + 'content': 'Please call the test function', + 'role': 'user', + 'type': 'message', + }), + dict({ + 'encrypted_content': 'AAABBB', + 'id': 'rs_A', + 'summary': list([ + dict({ + 'text': 'Thinking', + 'type': 'summary_text', + }), + dict({ + 'text': 'Thinking more', + 'type': 'summary_text', + }), + ]), + 'type': 'reasoning', + }), + dict({ + 'arguments': '{"param1": "call1"}', + 'call_id': 'call_call_1', + 'name': 'test_tool', + 'type': 'function_call', + }), + dict({ + 'call_id': 'call_call_1', + 'output': '"value1"', + 'type': 'function_call_output', + }), + dict({ + 'arguments': '{"param1": "call2"}', + 'call_id': 'call_call_2', + 'name': 'test_tool', + 'type': 'function_call', + }), + dict({ + 'call_id': 'call_call_2', + 'output': '"value2"', + 'type': 'function_call_output', + }), + dict({ + 'content': 'Cool', + 'role': 'assistant', + 'type': 'message', + }), + ]) +# --- # name: test_function_call_without_reasoning list([ dict({ @@ -66,9 +176,12 @@ dict({ 'agent_id': 'conversation.openai_conversation', 'content': None, + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': list([ dict({ + 'external': False, 'id': 'call_call_1', 'tool_args': dict({ 'param1': 'call1', @@ -87,8 +200,43 @@ dict({ 'agent_id': 'conversation.openai_conversation', 'content': 'Cool', + 'native': None, 'role': 'assistant', + 'thinking_content': None, 'tool_calls': None, }), ]) # --- +# name: test_web_search + list([ + dict({ + 'content': "What's on the latest news?", + 'role': 'user', + 'type': 'message', + }), + dict({ + 'action': dict({ + 'query': 'query', + 'type': 'search', + }), + 'id': 'ws_A', + 'status': 'completed', + 'type': 'web_search_call', + }), + dict({ + 'content': 'Home Assistant now supports ChatGPT Search in Assist', + 'role': 'assistant', + 'type': 'message', + }), + dict({ + 'content': 'Thank you!', + 'role': 'user', + 'type': 'message', + }), + dict({ + 'content': 'You are welcome!', + 'role': 'assistant', + 'type': 'message', + }), + ]) +# --- diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 6d8fb143f88..3f3b7801c8f 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -20,6 +20,7 @@ from homeassistant.components.openai_conversation.const import ( CONF_RECOMMENDED, CONF_TEMPERATURE, CONF_TOP_P, + CONF_VERBOSITY, CONF_WEB_SEARCH, CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_CONTEXT_SIZE, @@ -302,7 +303,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non ( { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pirate", + CONF_PROMPT: "Speak like a pro", }, { CONF_TEMPERATURE: 1.0, @@ -317,7 +318,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non ), { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pirate", + CONF_PROMPT: "Speak like a pro", CONF_TEMPERATURE: 1.0, CONF_CHAT_MODEL: "o1-pro", CONF_TOP_P: RECOMMENDED_TOP_P, @@ -414,35 +415,51 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non ( # Case 2: reasoning model { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pro", + CONF_PROMPT: "Speak like a pirate", CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o1-pro", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, - CONF_REASONING_EFFORT: "high", + CONF_REASONING_EFFORT: "low", + CONF_VERBOSITY: "high", + CONF_CODE_INTERPRETER: False, + CONF_WEB_SEARCH: False, + CONF_WEB_SEARCH_CONTEXT_SIZE: "low", + CONF_WEB_SEARCH_USER_LOCATION: False, }, ( { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pro", + CONF_PROMPT: "Speak like a pirate", }, { CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o1-pro", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, }, - {CONF_REASONING_EFFORT: "high", CONF_CODE_INTERPRETER: False}, + { + CONF_REASONING_EFFORT: "minimal", + CONF_CODE_INTERPRETER: False, + CONF_VERBOSITY: "high", + CONF_WEB_SEARCH: False, + CONF_WEB_SEARCH_CONTEXT_SIZE: "low", + CONF_WEB_SEARCH_USER_LOCATION: False, + }, ), { CONF_RECOMMENDED: False, - CONF_PROMPT: "Speak like a pro", + CONF_PROMPT: "Speak like a pirate", CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o1-pro", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, - CONF_REASONING_EFFORT: "high", + CONF_REASONING_EFFORT: "minimal", CONF_CODE_INTERPRETER: False, + CONF_VERBOSITY: "high", + CONF_WEB_SEARCH: False, + CONF_WEB_SEARCH_CONTEXT_SIZE: "low", + CONF_WEB_SEARCH_USER_LOCATION: False, }, ), # Test that old options are removed after reconfiguration @@ -482,11 +499,13 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non CONF_PROMPT: "Speak like a pirate", CONF_LLM_HASS_API: ["assist"], CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "gpt-4o", + CONF_CHAT_MODEL: "gpt-5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, CONF_REASONING_EFFORT: "high", CONF_CODE_INTERPRETER: True, + CONF_VERBOSITY: "low", + CONF_WEB_SEARCH: False, }, ( { @@ -550,11 +569,12 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non CONF_PROMPT: "Speak like a pirate", CONF_LLM_HASS_API: ["assist"], CONF_TEMPERATURE: 0.8, - CONF_CHAT_MODEL: "o3-mini", + CONF_CHAT_MODEL: "o5", CONF_TOP_P: 0.9, CONF_MAX_TOKENS: 1000, CONF_REASONING_EFFORT: "low", CONF_CODE_INTERPRETER: True, + CONF_VERBOSITY: "medium", }, ( { diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index dafcba7bfeb..452404f65ac 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -252,7 +252,11 @@ async def test_function_call( # Initial conversation ( # Wait for the model to think - *create_reasoning_item(id="rs_A", output_index=0), + *create_reasoning_item( + id="rs_A", + output_index=0, + reasoning_summary=[["Thinking"], ["Thinking ", "more"]], + ), # First tool call *create_function_tool_call_item( id="fc_1", @@ -288,15 +292,10 @@ async def test_function_call( agent_id="conversation.openai_conversation", ) - assert mock_create_stream.call_args.kwargs["input"][2] == { - "id": "rs_A", - "summary": [], - "type": "reasoning", - "encrypted_content": "AAABBB", - } assert result.response.response_type == intent.IntentResponseType.ACTION_DONE # Don't test the prompt, as it's not deterministic assert mock_chat_log.content[1:] == snapshot + assert mock_create_stream.call_args.kwargs["input"][1:] == snapshot async def test_function_call_without_reasoning( @@ -436,6 +435,7 @@ async def test_web_search( mock_init_component, mock_create_stream, mock_chat_log: MockChatLog, # noqa: F811 + snapshot: SnapshotAssertion, ) -> None: """Test web_search_tool.""" subentry = next(iter(mock_config_entry.subentries.values())) @@ -488,6 +488,21 @@ async def test_web_search( assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.speech["plain"]["speech"] == message, result.response.speech + # Test follow-up message in multi-turn conversation + mock_create_stream.return_value = [ + (*create_message_item(id="msg_B", text="You are welcome!", output_index=1),) + ] + + result = await conversation.async_converse( + hass, + "Thank you!", + mock_chat_log.conversation_id, + Context(), + agent_id="conversation.openai_conversation", + ) + + assert mock_create_stream.mock_calls[1][2]["input"][1:] == snapshot + async def test_code_interpreter( hass: HomeAssistant, @@ -495,6 +510,7 @@ async def test_code_interpreter( mock_init_component, mock_create_stream, mock_chat_log: MockChatLog, # noqa: F811 + snapshot: SnapshotAssertion, ) -> None: """Test code_interpreter tool.""" subentry = next(iter(mock_config_entry.subentries.values())) @@ -514,6 +530,7 @@ async def test_code_interpreter( *create_code_interpreter_item( id="ci_A", code=["import", " math", "\n", "math", ".sqrt", "(", "555", "55", ")"], + logs="235.70108188126758\n", output_index=0, ), *create_message_item(id="msg_A", text=message, output_index=1), @@ -533,3 +550,18 @@ async def test_code_interpreter( ] assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert result.response.speech["plain"]["speech"] == message, result.response.speech + + # Test follow-up message in multi-turn conversation + mock_create_stream.return_value = [ + (*create_message_item(id="msg_B", text="You are welcome!", output_index=1),) + ] + + result = await conversation.async_converse( + hass, + "Thank you!", + mock_chat_log.conversation_id, + Context(), + agent_id="conversation.openai_conversation", + ) + + assert mock_create_stream.mock_calls[1][2]["input"][1:] == snapshot diff --git a/tests/components/openai_conversation/test_entity.py b/tests/components/openai_conversation/test_entity.py index 58187bd63e9..c24cb5b3d79 100644 --- a/tests/components/openai_conversation/test_entity.py +++ b/tests/components/openai_conversation/test_entity.py @@ -63,6 +63,8 @@ async def test_format_structured_output() -> None: "item_value", ], "type": "object", + "additionalProperties": False, + "strict": True, }, "type": "array", }, diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index fb8be3b2e68..66afc41826b 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -94,6 +94,7 @@ async def test_generate_image_service( with patch( "openai.resources.images.AsyncImages.generate", + new_callable=AsyncMock, return_value=ImagesResponse( created=1700000000, data=[ @@ -130,6 +131,7 @@ async def test_generate_image_service_error( with ( patch( "openai.resources.images.AsyncImages.generate", + new_callable=AsyncMock, side_effect=RateLimitError( response=httpx.Response( status_code=500, request=httpx.Request(method="GET", url="") @@ -154,6 +156,7 @@ async def test_generate_image_service_error( with ( patch( "openai.resources.images.AsyncImages.generate", + new_callable=AsyncMock, return_value=ImagesResponse( created=1700000000, data=[ diff --git a/tests/components/openweathermap/conftest.py b/tests/components/openweathermap/conftest.py index f7de53b8f97..7c7de776acf 100644 --- a/tests/components/openweathermap/conftest.py +++ b/tests/components/openweathermap/conftest.py @@ -77,8 +77,8 @@ def owm_client_mock() -> Generator[AsyncMock]: cloud_coverage=75, visibility=10000, wind_speed=9.83, + wind_gust=11.81, wind_bearing=199, - wind_gust=None, rain={"1h": 1.21}, snow=None, condition=WeatherCondition( diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index 11a1feb721f..ae80431f33c 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -86,7 +86,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-co', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state] @@ -96,7 +96,7 @@ 'device_class': 'carbon_monoxide', 'friendly_name': 'openweathermap Carbon monoxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_carbon_monoxide', @@ -140,7 +140,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-no2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_dioxide-state] @@ -150,7 +150,7 @@ 'device_class': 'nitrogen_dioxide', 'friendly_name': 'openweathermap Nitrogen dioxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_nitrogen_dioxide', @@ -194,7 +194,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-no', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_monoxide-state] @@ -204,7 +204,7 @@ 'device_class': 'nitrogen_monoxide', 'friendly_name': 'openweathermap Nitrogen monoxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_nitrogen_monoxide', @@ -248,7 +248,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-o3', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_ozone-state] @@ -258,7 +258,7 @@ 'device_class': 'ozone', 'friendly_name': 'openweathermap Ozone', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_ozone', @@ -302,7 +302,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-pm10', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_pm10-state] @@ -312,7 +312,7 @@ 'device_class': 'pm10', 'friendly_name': 'openweathermap PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_pm10', @@ -356,7 +356,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-pm2_5', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_pm2_5-state] @@ -366,7 +366,7 @@ 'device_class': 'pm25', 'friendly_name': 'openweathermap PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_pm2_5', @@ -410,7 +410,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-so2', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_sulphur_dioxide-state] @@ -420,7 +420,7 @@ 'device_class': 'sulphur_dioxide', 'friendly_name': 'openweathermap Sulphur dioxide', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_sulphur_dioxide', @@ -1239,6 +1239,66 @@ 'state': '199', }) # --- +# name: test_sensor_states[current][sensor.openweathermap_wind_gust-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_gust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_wind_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'wind_speed', + 'friendly_name': 'openweathermap Wind gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.516', + }) +# --- # name: test_sensor_states[current][sensor.openweathermap_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2108,6 +2168,66 @@ 'state': '199', }) # --- +# name: test_sensor_states[v3.0][sensor.openweathermap_wind_gust-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_gust', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wind gust', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_gust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_wind_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'wind_speed', + 'friendly_name': 'openweathermap Wind gust', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.516', + }) +# --- # name: test_sensor_states[v3.0][sensor.openweathermap_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/openweathermap/snapshots/test_weather.ambr b/tests/components/openweathermap/snapshots/test_weather.ambr index 760160a96f4..073715c87ec 100644 --- a/tests/components/openweathermap/snapshots/test_weather.ambr +++ b/tests/components/openweathermap/snapshots/test_weather.ambr @@ -74,6 +74,7 @@ 'temperature_unit': , 'visibility_unit': , 'wind_bearing': 199, + 'wind_gust_speed': 42.52, 'wind_speed': 35.39, 'wind_speed_unit': , }), @@ -137,6 +138,7 @@ 'temperature_unit': , 'visibility_unit': , 'wind_bearing': 199, + 'wind_gust_speed': 42.52, 'wind_speed': 35.39, 'wind_speed_unit': , }), @@ -200,6 +202,7 @@ 'temperature_unit': , 'visibility_unit': , 'wind_bearing': 199, + 'wind_gust_speed': 42.52, 'wind_speed': 35.39, 'wind_speed_unit': , }), diff --git a/tests/components/opower/test_repairs.py b/tests/components/opower/test_repairs.py new file mode 100644 index 00000000000..7f589be6a26 --- /dev/null +++ b/tests/components/opower/test_repairs.py @@ -0,0 +1,82 @@ +"""Test the Opower repairs.""" + +from homeassistant.components.opower.const import DOMAIN +from homeassistant.components.recorder import Recorder +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.repairs import ( + async_process_repairs_platforms, + process_repair_fix_flow, + start_repair_fix_flow, +) +from tests.typing import ClientSessionGenerator + + +async def test_unsupported_utility_fix_flow( + recorder_mock: Recorder, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the unsupported utility fix flow.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "utility": "Unsupported Utility", + "username": "test-user", + "password": "test-password", + }, + title="My Unsupported Utility", + ) + mock_config_entry.add_to_hass(hass) + + # Setting up the component with an unsupported utility should fail and create an issue + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + # Verify the issue was created correctly + issue_id = f"unsupported_utility_{mock_config_entry.entry_id}" + issue = issue_registry.async_get_issue(DOMAIN, issue_id) + assert issue is not None + assert issue.translation_key == "unsupported_utility" + assert issue.is_fixable is True + assert issue.data == { + "entry_id": mock_config_entry.entry_id, + "utility": "Unsupported Utility", + "title": "My Unsupported Utility", + } + + await async_process_repairs_platforms(hass) + http_client = await hass_client() + + # Start the repair flow + data = await start_repair_fix_flow(http_client, DOMAIN, issue_id) + flow_id = data["flow_id"] + + # The flow should go directly to the confirm step + assert data["step_id"] == "confirm" + assert data["description_placeholders"] == { + "utility": "Unsupported Utility", + "title": "My Unsupported Utility", + } + + # Submit the confirmation form + data = await process_repair_fix_flow(http_client, flow_id, json={}) + + # The flow should complete and create an empty entry, signaling success + assert data["type"] == "create_entry" + + await hass.async_block_till_done() + + # Check that the config entry has been removed + assert hass.config_entries.async_get_entry(mock_config_entry.entry_id) is None + # Check that the issue has been resolved + assert not issue_registry.async_get_issue(DOMAIN, issue_id) diff --git a/tests/components/paperless_ngx/test_sensor.py b/tests/components/paperless_ngx/test_sensor.py index d2233a64ee2..5b5827bca37 100644 --- a/tests/components/paperless_ngx/test_sensor.py +++ b/tests/components/paperless_ngx/test_sensor.py @@ -29,6 +29,7 @@ from tests.common import ( ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_platform( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index c4dcc44e619..77227fd0f63 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -125,7 +125,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_tv.setTransport.assert_called_with(True) + mock_tv.setTransport.assert_called_with(True, ANY) mock_tv.pairRequest.assert_called() result = await hass.config_entries.flow.async_configure( @@ -204,7 +204,7 @@ async def test_pair_grant_failed( assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_tv.setTransport.assert_called_with(True) + mock_tv.setTransport.assert_called_with(True, ANY) mock_tv.pairRequest.assert_called() # Test with invalid pin @@ -266,6 +266,7 @@ async def test_zeroconf_discovery( """Test we can setup from zeroconf discovery.""" mock_tv_pairable.secured_transport = secured_transport + mock_tv_pairable.api_version_detected = 6 result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -291,7 +292,7 @@ async def test_zeroconf_discovery( assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_tv_pairable.setTransport.assert_called_with(secured_transport) + mock_tv_pairable.setTransport.assert_called_with(secured_transport, 6) mock_tv_pairable.pairRequest.assert_called() diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index c20f22ac58d..c2edb51e066 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -221,12 +221,16 @@ def _create_mocked_hole( if wrong_host: raise HoleConnectionError("Cannot authenticate with Pi-hole: err") password = getattr(mocked_hole, "password", None) + if ( raise_exception or incorrect_app_password + or api_version == 5 or (api_version == 6 and password not in ["newkey", "apikey"]) ): - if api_version == 6: + if api_version == 6 and ( + incorrect_app_password or password not in ["newkey", "apikey"] + ): raise HoleError("Authentication failed: Invalid password") raise HoleConnectionError diff --git a/tests/components/playstation_network/test_config_flow.py b/tests/components/playstation_network/test_config_flow.py index 4194f1fb258..0cd94fe153a 100644 --- a/tests/components/playstation_network/test_config_flow.py +++ b/tests/components/playstation_network/test_config_flow.py @@ -493,3 +493,27 @@ async def test_add_friend_flow_already_configured_as_entry( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_as_entry" + + +async def test_add_friend_flow_no_friends( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_psnawpapi: MagicMock, +) -> None: + """Test we abort add friend subentry flow when the user has no friends.""" + + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_psnawpapi.user.return_value.friends_list.return_value = [] + + result = await hass.config_entries.subentries.async_init( + (config_entry.entry_id, "friend"), + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_friends" diff --git a/tests/components/plugwise/snapshots/test_binary_sensor.ambr b/tests/components/plugwise/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..d371bb38803 --- /dev/null +++ b/tests/components/plugwise/snapshots/test_binary_sensor.ambr @@ -0,0 +1,947 @@ +# serializer version: 1 +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.adam_plugwise_notification-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.adam_plugwise_notification', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Plugwise notification', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plugwise_notification', + 'unique_id': 'fe799307f1624099878210aa0b9f1475-plugwise_notification', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.adam_plugwise_notification-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'error_msg': list([ + ]), + 'friendly_name': 'Adam Plugwise notification', + 'info_msg': list([ + ]), + 'other_msg': list([ + ]), + 'warning_msg': list([ + "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", + ]), + }), + 'context': , + 'entity_id': 'binary_sensor.adam_plugwise_notification', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.bios_cv_thermostatic_radiator_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bios_cv_thermostatic_radiator_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a2c3583e0a6349358998b760cea82d2a-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.bios_cv_thermostatic_radiator_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Bios Cv Thermostatic Radiator Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.bios_cv_thermostatic_radiator_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.cv_kraan_garage_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cv_kraan_garage_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'e7693eb9582644e5b865dba8d4447cf1-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.cv_kraan_garage_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'CV Kraan Garage Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.cv_kraan_garage_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.onoff_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.onoff_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heating_state', + 'unique_id': '90986d591dcd426cae3ec3e8111ff730-heating_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.onoff_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OnOff Heating', + }), + 'context': , + 'entity_id': 'binary_sensor.onoff_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.thermostatic_radiator_badkamer_1_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.thermostatic_radiator_badkamer_1_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '680423ff840043738f42cc7f1ff97a36-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.thermostatic_radiator_badkamer_1_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Thermostatic Radiator Badkamer 1 Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.thermostatic_radiator_badkamer_1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.thermostatic_radiator_badkamer_2_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.thermostatic_radiator_badkamer_2_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'f1fee6043d3642a9b0a65297455f008e-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.thermostatic_radiator_badkamer_2_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Thermostatic Radiator Badkamer 2 Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.thermostatic_radiator_badkamer_2_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.thermostatic_radiator_jessie_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.thermostatic_radiator_jessie_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'd3da73bde12a47d5a6b8f9dad971f2ec-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.thermostatic_radiator_jessie_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Thermostatic Radiator Jessie Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.thermostatic_radiator_jessie_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.zone_lisa_bios_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_lisa_bios_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'df4a4a8169904cdb9c03d61a21f42140-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.zone_lisa_bios_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Zone Lisa Bios Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_lisa_bios_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.zone_lisa_wk_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_lisa_wk_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'b59bcebaf94b499ea7d46e4a66fb62d8-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.zone_lisa_wk_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Zone Lisa WK Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_lisa_wk_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.zone_thermostat_jessie_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_thermostat_jessie_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '6a3bf693d05e48e0b460c815a4fdd09d-low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_binary_sensor_snapshot[platforms0][binary_sensor.zone_thermostat_jessie_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Zone Thermostat Jessie Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_thermostat_jessie_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_compressor_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.opentherm_compressor_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Compressor state', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'compressor_state', + 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-compressor_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_compressor_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OpenTherm Compressor state', + }), + 'context': , + 'entity_id': 'binary_sensor.opentherm_compressor_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_cooling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.opentherm_cooling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cooling', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cooling_state', + 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-cooling_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_cooling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OpenTherm Cooling', + }), + 'context': , + 'entity_id': 'binary_sensor.opentherm_cooling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_cooling_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.opentherm_cooling_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cooling enabled', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cooling_enabled', + 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-cooling_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_cooling_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OpenTherm Cooling enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.opentherm_cooling_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_dhw_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.opentherm_dhw_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'DHW state', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dhw_state', + 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-dhw_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_dhw_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OpenTherm DHW state', + }), + 'context': , + 'entity_id': 'binary_sensor.opentherm_dhw_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_flame_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.opentherm_flame_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flame state', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flame_state', + 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-flame_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_flame_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OpenTherm Flame state', + }), + 'context': , + 'entity_id': 'binary_sensor.opentherm_flame_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.opentherm_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heating_state', + 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-heating_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OpenTherm Heating', + }), + 'context': , + 'entity_id': 'binary_sensor.opentherm_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_secondary_boiler_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.opentherm_secondary_boiler_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Secondary boiler state', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'secondary_boiler_state', + 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-secondary_boiler_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.opentherm_secondary_boiler_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'OpenTherm Secondary boiler state', + }), + 'context': , + 'entity_id': 'binary_sensor.opentherm_secondary_boiler_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.smile_anna_plugwise_notification-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.smile_anna_plugwise_notification', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Plugwise notification', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plugwise_notification', + 'unique_id': '015ae9ea3f964e668e490fa39da3870b-plugwise_notification', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_binary_sensor_snapshot[platforms0-True-anna_heatpump_heating][binary_sensor.smile_anna_plugwise_notification-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'error_msg': list([ + ]), + 'friendly_name': 'Smile Anna Plugwise notification', + 'info_msg': list([ + ]), + 'other_msg': list([ + ]), + 'warning_msg': list([ + ]), + }), + 'context': , + 'entity_id': 'binary_sensor.smile_anna_plugwise_notification', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_p1_v4_binary_sensor_snapshot[platforms0-03e65b16e4b247a29ae0d75a78cb492e-p1v4_442_triple][binary_sensor.smile_p1_plugwise_notification-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.smile_p1_plugwise_notification', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Plugwise notification', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'plugwise_notification', + 'unique_id': '03e65b16e4b247a29ae0d75a78cb492e-plugwise_notification', + 'unit_of_measurement': None, + }) +# --- +# name: test_p1_v4_binary_sensor_snapshot[platforms0-03e65b16e4b247a29ae0d75a78cb492e-p1v4_442_triple][binary_sensor.smile_p1_plugwise_notification-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'error_msg': list([ + ]), + 'friendly_name': 'Smile P1 Plugwise notification', + 'info_msg': list([ + ]), + 'other_msg': list([ + ]), + 'warning_msg': list([ + 'The Smile P1 is not connected to a smart meter.', + ]), + }), + 'context': , + 'entity_id': 'binary_sensor.smile_p1_plugwise_notification', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 7bf475086af..c01da5c5205 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -3,36 +3,43 @@ from unittest.mock import MagicMock import pytest +from syrupy.assertion import SnapshotAssertion +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize("platforms", [(BINARY_SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_adam_binary_sensor_snapshot( + hass: HomeAssistant, + mock_smile_adam: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, +) -> None: + """Test Adam binary_sensor snapshot.""" + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) @pytest.mark.parametrize("cooling_present", [True], indirect=True) -@pytest.mark.parametrize( - ("entity_id", "expected_state"), - [ - ("binary_sensor.opentherm_secondary_boiler_state", STATE_OFF), - ("binary_sensor.opentherm_dhw_state", STATE_OFF), - ("binary_sensor.opentherm_heating", STATE_ON), - ("binary_sensor.opentherm_cooling_enabled", STATE_OFF), - ("binary_sensor.opentherm_compressor_state", STATE_ON), - ], -) -async def test_anna_climate_binary_sensor_entities( +@pytest.mark.parametrize("platforms", [(BINARY_SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_anna_binary_sensor_snapshot( hass: HomeAssistant, mock_smile_anna: MagicMock, - init_integration: MockConfigEntry, - entity_id: str, - expected_state: str, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, ) -> None: - """Test creation of climate related binary_sensor entities.""" - state = hass.states.get(entity_id) - assert state.state == expected_state + """Test Anna binary_sensor snapshot.""" + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) @@ -49,35 +56,23 @@ async def test_anna_climate_binary_sensor_change( assert state.state == STATE_ON await async_update_entity(hass, "binary_sensor.opentherm_dhw_state") - state = hass.states.get("binary_sensor.opentherm_dhw_state") assert state assert state.state == STATE_OFF -async def test_adam_climate_binary_sensor_change( - hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry -) -> None: - """Test of a climate related plugwise-notification binary_sensor.""" - state = hass.states.get("binary_sensor.adam_plugwise_notification") - assert state - assert state.state == STATE_ON - assert "warning_msg" in state.attributes - assert "unreachable" in state.attributes["warning_msg"][0] - assert not state.attributes.get("error_msg") - assert not state.attributes.get("other_msg") - - @pytest.mark.parametrize("chosen_env", ["p1v4_442_triple"], indirect=True) @pytest.mark.parametrize( "gateway_id", ["03e65b16e4b247a29ae0d75a78cb492e"], indirect=True ) -async def test_p1_binary_sensor_entity( - hass: HomeAssistant, mock_smile_p1: MagicMock, init_integration: MockConfigEntry +@pytest.mark.parametrize("platforms", [(BINARY_SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_p1_v4_binary_sensor_snapshot( + hass: HomeAssistant, + mock_smile_p1: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, ) -> None: - """Test of a Smile P1 related plugwise-notification binary_sensor.""" - state = hass.states.get("binary_sensor.smile_p1_plugwise_notification") - assert state - assert state.state == STATE_ON - assert "warning_msg" in state.attributes - assert "connected" in state.attributes["warning_msg"][0] + """Test Smile P1 binary_sensor snapshot.""" + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 8dcc6917346..c41c88ec950 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -12,9 +12,9 @@ from tests.common import MockConfigEntry mock_value_step_user = { "title": "1R & 1IN Board", - "relay_count": 1, - "input_count": 1, - "is_old": False, + "relays": 1, + "inputs": 1, + "temps": False, } diff --git a/tests/components/qbus/snapshots/test_binary_sensor.ambr b/tests/components/qbus/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..79b36db6639 --- /dev/null +++ b/tests/components/qbus/snapshots/test_binary_sensor.ambr @@ -0,0 +1,146 @@ +# serializer version: 1 +# name: test_binary_sensor[binary_sensor.ctd_000001-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.ctd_000001', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ctd_000001_connected', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.ctd_000001-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'CTD 000001', + }), + 'context': , + 'entity_id': 'binary_sensor.ctd_000001', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.weersensor_raining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.weersensor_raining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Raining', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'raining', + 'unique_id': 'ctd_000001_21007_raining', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.weersensor_raining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Weersensor Raining', + }), + 'context': , + 'entity_id': 'binary_sensor.weersensor_raining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.weersensor_twilight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.weersensor_twilight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Twilight', + 'platform': 'qbus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'twilight', + 'unique_id': 'ctd_000001_21007_twilight', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.weersensor_twilight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Weersensor Twilight', + }), + 'context': , + 'entity_id': 'binary_sensor.weersensor_twilight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/qbus/test_binary_sensor.py b/tests/components/qbus/test_binary_sensor.py new file mode 100644 index 00000000000..9160bdb916e --- /dev/null +++ b/tests/components/qbus/test_binary_sensor.py @@ -0,0 +1,27 @@ +"""Test Qbus binary sensors.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_binary_sensor( + hass: HomeAssistant, + setup_integration_deferred: Callable[[], Awaitable], + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test binary sensor.""" + + with patch("homeassistant.components.qbus.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration_deferred() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index b4dd513c317..c5ba1c77c31 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -125,7 +125,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 800d090fd7b..0321ba8bbaa 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -125,7 +125,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 48b024e0b10..f8134a515e0 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -43,7 +43,6 @@ TEST_PORT = 1234 TEST_NVR_NAME = "test_reolink_name" TEST_CAM_NAME = "test_reolink_cam" TEST_NVR_NAME2 = "test2_reolink_name" -TEST_CAM_NAME = "test_reolink_cam" TEST_USE_HTTPS = True TEST_HOST_MODEL = "RLN8-410" TEST_ITEM_NUMBER = "P000" @@ -117,7 +116,7 @@ def _init_host_mock(host_mock: MagicMock) -> None: host_mock.supported.return_value = True host_mock.item_number.return_value = TEST_ITEM_NUMBER host_mock.camera_model.return_value = TEST_CAM_MODEL - host_mock.camera_name.return_value = TEST_NVR_NAME + host_mock.camera_name.return_value = TEST_CAM_NAME host_mock.camera_hardware_version.return_value = "IPC_00001" host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" host_mock.camera_sw_version_update_required.return_value = False diff --git a/tests/components/reolink/test_binary_sensor.py b/tests/components/reolink/test_binary_sensor.py index e6275a2108e..4bbe222fad6 100644 --- a/tests/components/reolink/test_binary_sensor.py +++ b/tests/components/reolink/test_binary_sensor.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant -from .conftest import TEST_DUO_MODEL, TEST_HOST_MODEL, TEST_NVR_NAME +from .conftest import TEST_CAM_NAME, TEST_DUO_MODEL, TEST_HOST_MODEL from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import ClientSessionGenerator @@ -31,7 +31,7 @@ async def test_motion_sensor( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion_lens_0" + entity_id = f"{Platform.BINARY_SENSOR}.{TEST_CAM_NAME}_motion_lens_0" assert hass.states.get(entity_id).state == STATE_ON reolink_host.motion_detected.return_value = False @@ -66,7 +66,7 @@ async def test_smart_ai_sensor( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_crossline_zone1_person" + entity_id = f"{Platform.BINARY_SENSOR}.{TEST_CAM_NAME}_crossline_zone1_person" assert hass.states.get(entity_id).state == STATE_ON reolink_host.baichuan.smart_ai_state.return_value = False @@ -106,7 +106,7 @@ async def test_tcp_callback( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion" + entity_id = f"{Platform.BINARY_SENSOR}.{TEST_CAM_NAME}_motion" assert hass.states.get(entity_id).state == STATE_ON # simulate a TCP push callback diff --git a/tests/components/reolink/test_button.py b/tests/components/reolink/test_button.py index ee51d0f0b99..1e773491938 100644 --- a/tests/components/reolink/test_button.py +++ b/tests/components/reolink/test_button.py @@ -13,7 +13,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_NAME, TEST_NVR_NAME from tests.common import MockConfigEntry @@ -29,7 +29,7 @@ async def test_button( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.BUTTON}.{TEST_NVR_NAME}_ptz_up" + entity_id = f"{Platform.BUTTON}.{TEST_CAM_NAME}_ptz_up" await hass.services.async_call( BUTTON_DOMAIN, @@ -60,7 +60,7 @@ async def test_ptz_move_service( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.BUTTON}.{TEST_NVR_NAME}_ptz_up" + entity_id = f"{Platform.BUTTON}.{TEST_CAM_NAME}_ptz_up" await hass.services.async_call( DOMAIN, diff --git a/tests/components/reolink/test_camera.py b/tests/components/reolink/test_camera.py index 4ab43de225f..99236526070 100644 --- a/tests/components/reolink/test_camera.py +++ b/tests/components/reolink/test_camera.py @@ -15,7 +15,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from .conftest import TEST_DUO_MODEL, TEST_NVR_NAME +from .conftest import TEST_CAM_NAME, TEST_DUO_MODEL from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator @@ -33,7 +33,7 @@ async def test_camera( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.CAMERA}.{TEST_NVR_NAME}_fluent" + entity_id = f"{Platform.CAMERA}.{TEST_CAM_NAME}_fluent" assert hass.states.get(entity_id).state == CameraState.IDLE # check getting a image from the camera @@ -63,5 +63,5 @@ async def test_camera_no_stream_source( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.CAMERA}.{TEST_NVR_NAME}_snapshots_fluent_lens_0" + entity_id = f"{Platform.CAMERA}.{TEST_CAM_NAME}_snapshots_fluent_lens_0" assert hass.states.get(entity_id).state == CameraState.IDLE diff --git a/tests/components/reolink/test_host.py b/tests/components/reolink/test_host.py index 6ae7c66704c..194d038a32a 100644 --- a/tests/components/reolink/test_host.py +++ b/tests/components/reolink/test_host.py @@ -28,7 +28,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.network import NoURLAvailableError from homeassistant.util.aiohttp import MockRequest -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_NAME from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -92,7 +92,7 @@ async def test_webhook_callback( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion" + entity_id = f"{Platform.BINARY_SENSOR}.{TEST_CAM_NAME}_motion" webhook_id = config_entry.runtime_data.host.webhook_id unique_id = config_entry.runtime_data.host.unique_id diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 10eefccace9..662469ebc01 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -52,6 +52,7 @@ from .conftest import ( DEFAULT_PROTOCOL, TEST_BC_PORT, TEST_CAM_MODEL, + TEST_CAM_NAME, TEST_HOST, TEST_HOST_MODEL, TEST_MAC, @@ -1034,7 +1035,7 @@ async def test_privacy_mode_change_callback( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio" + entity_id = f"{Platform.SWITCH}.{TEST_CAM_NAME}_record_audio" assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # simulate a TCP push callback signaling a privacy mode change @@ -1106,7 +1107,7 @@ async def test_camera_wake_callback( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio" + entity_id = f"{Platform.SWITCH}.{TEST_CAM_NAME}_record_audio" assert hass.states.get(entity_id).state == STATE_ON reolink_host.sleeping.return_value = False diff --git a/tests/components/reolink/test_light.py b/tests/components/reolink/test_light.py index c3655ec00df..80a0a7abeab 100644 --- a/tests/components/reolink/test_light.py +++ b/tests/components/reolink/test_light.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_NAME, TEST_NVR_NAME from tests.common import MockConfigEntry @@ -45,7 +45,7 @@ async def test_light_state( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.LIGHT}.{TEST_NVR_NAME}_floodlight" + entity_id = f"{Platform.LIGHT}.{TEST_CAM_NAME}_floodlight" state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -63,7 +63,7 @@ async def test_light_turn_off( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.LIGHT}.{TEST_NVR_NAME}_floodlight" + entity_id = f"{Platform.LIGHT}.{TEST_CAM_NAME}_floodlight" await hass.services.async_call( LIGHT_DOMAIN, @@ -94,7 +94,7 @@ async def test_light_turn_on( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.LIGHT}.{TEST_NVR_NAME}_floodlight" + entity_id = f"{Platform.LIGHT}.{TEST_CAM_NAME}_floodlight" await hass.services.async_call( LIGHT_DOMAIN, @@ -128,7 +128,7 @@ async def test_light_turn_on_errors( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.LIGHT}.{TEST_NVR_NAME}_floodlight" + entity_id = f"{Platform.LIGHT}.{TEST_CAM_NAME}_floodlight" reolink_host.set_whiteled.side_effect = exception with pytest.raises(HomeAssistantError): diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 0308639499c..c8bc8fd9c70 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -34,10 +34,10 @@ from homeassistant.setup import async_setup_component from .conftest import ( TEST_BC_PORT, + TEST_CAM_NAME, TEST_HOST2, TEST_HOST_MODEL, TEST_MAC2, - TEST_NVR_NAME, TEST_NVR_NAME2, TEST_PASSWORD2, TEST_PORT, @@ -61,7 +61,6 @@ TEST_FILE_NAME = f"{TEST_START}00" TEST_FILE_NAME_MP4 = f"{TEST_START}00.mp4" TEST_STREAM = "main" TEST_CHANNEL = "0" -TEST_CAM_NAME = "Cam new name" TEST_MIME_TYPE = "application/x-mpegURL" TEST_MIME_TYPE_MP4 = "video/mp4" @@ -172,7 +171,7 @@ async def test_browsing( browse_res_AT_sub_id = f"RES|{entry_id}|{TEST_CHANNEL}|autotrack_sub" browse_res_AT_main_id = f"RES|{entry_id}|{TEST_CHANNEL}|autotrack_main" assert browse.domain == DOMAIN - assert browse.title == f"{TEST_NVR_NAME} lens 0" + assert browse.title == f"{TEST_CAM_NAME} lens 0" assert browse.identifier == browse_resolution_id assert browse.children[0].identifier == browse_res_sub_id assert browse.children[1].identifier == browse_res_main_id @@ -188,19 +187,19 @@ async def test_browsing( browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_sub_id}") assert browse.domain == DOMAIN - assert browse.title == f"{TEST_NVR_NAME} lens 0 Low res." + assert browse.title == f"{TEST_CAM_NAME} lens 0 Low res." browse = await async_browse_media( hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_AT_sub_id}" ) assert browse.domain == DOMAIN - assert browse.title == f"{TEST_NVR_NAME} lens 0 Telephoto low res." + assert browse.title == f"{TEST_CAM_NAME} lens 0 Telephoto low res." browse = await async_browse_media( hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_AT_main_id}" ) assert browse.domain == DOMAIN - assert browse.title == f"{TEST_NVR_NAME} lens 0 Telephoto high res." + assert browse.title == f"{TEST_CAM_NAME} lens 0 Telephoto high res." browse = await async_browse_media( hass, f"{URI_SCHEME}{DOMAIN}/{browse_res_main_id}" @@ -210,7 +209,7 @@ async def test_browsing( browse_day_0_id = f"DAY|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_YEAR}|{TEST_MONTH}|{TEST_DAY}" browse_day_1_id = f"DAY|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_YEAR}|{TEST_MONTH}|{TEST_DAY2}" assert browse.domain == DOMAIN - assert browse.title == f"{TEST_NVR_NAME} lens 0 High res." + assert browse.title == f"{TEST_CAM_NAME} lens 0 High res." assert browse.identifier == browse_days_id assert browse.children[0].identifier == browse_day_0_id assert browse.children[1].identifier == browse_day_1_id @@ -232,7 +231,7 @@ async def test_browsing( assert browse.domain == DOMAIN assert ( browse.title - == f"{TEST_NVR_NAME} lens 0 High res. {TEST_YEAR}/{TEST_MONTH}/{TEST_DAY}" + == f"{TEST_CAM_NAME} lens 0 High res. {TEST_YEAR}/{TEST_MONTH}/{TEST_DAY}" ) assert browse.identifier == browse_files_id assert browse.children[0].identifier == browse_file_id @@ -261,7 +260,7 @@ async def test_browsing( assert browse.domain == DOMAIN assert ( browse.title - == f"{TEST_NVR_NAME} High res. {TEST_YEAR}/{TEST_MONTH}/{TEST_DAY} Person" + == f"{TEST_CAM_NAME} High res. {TEST_YEAR}/{TEST_MONTH}/{TEST_DAY} Person" ) assert browse.identifier == browse_files_id assert browse.children[0].identifier == browse_file_id @@ -306,7 +305,7 @@ async def test_browsing_h265_encoding( browse_res_main_id = f"RES|{entry_id}|{TEST_CHANNEL}|main" assert browse.domain == DOMAIN - assert browse.title == f"{TEST_NVR_NAME}" + assert browse.title == f"{TEST_CAM_NAME}" assert browse.identifier == browse_resolution_id assert browse.children[0].identifier == browse_res_sub_id assert browse.children[1].identifier == browse_res_main_id @@ -321,7 +320,7 @@ async def test_browsing_h265_encoding( f"DAY|{entry_id}|{TEST_CHANNEL}|sub|{TEST_YEAR}|{TEST_MONTH}|{TEST_DAY2}" ) assert browse.domain == DOMAIN - assert browse.title == f"{TEST_NVR_NAME} Low res." + assert browse.title == f"{TEST_CAM_NAME} Low res." assert browse.identifier == browse_days_id assert browse.children[0].identifier == browse_day_0_id assert browse.children[1].identifier == browse_day_1_id diff --git a/tests/components/reolink/test_number.py b/tests/components/reolink/test_number.py index 17fc2797479..853edeefa5a 100644 --- a/tests/components/reolink/test_number.py +++ b/tests/components/reolink/test_number.py @@ -16,7 +16,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_NAME, TEST_NVR_NAME from tests.common import MockConfigEntry @@ -34,7 +34,7 @@ async def test_number( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.NUMBER}.{TEST_NVR_NAME}_volume" + entity_id = f"{Platform.NUMBER}.{TEST_CAM_NAME}_volume" assert hass.states.get(entity_id).state == "80" @@ -79,7 +79,7 @@ async def test_smart_ai_number( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.NUMBER}.{TEST_NVR_NAME}_AI_crossline_zone1_sensitivity" + entity_id = f"{Platform.NUMBER}.{TEST_CAM_NAME}_AI_crossline_zone1_sensitivity" assert hass.states.get(entity_id).state == "80" diff --git a/tests/components/reolink/test_select.py b/tests/components/reolink/test_select.py index fb0f98a6e31..5dcce747518 100644 --- a/tests/components/reolink/test_select.py +++ b/tests/components/reolink/test_select.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_NAME, TEST_NVR_NAME from tests.common import MockConfigEntry, async_fire_time_changed @@ -38,7 +38,7 @@ async def test_floodlight_mode_select( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_floodlight_mode" + entity_id = f"{Platform.SELECT}.{TEST_CAM_NAME}_floodlight_mode" assert hass.states.get(entity_id).state == "auto" await hass.services.async_call( @@ -88,7 +88,7 @@ async def test_play_quick_reply_message( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_play_quick_reply_message" + entity_id = f"{Platform.SELECT}.{TEST_CAM_NAME}_play_quick_reply_message" assert hass.states.get(entity_id).state == STATE_UNKNOWN await hass.services.async_call( diff --git a/tests/components/reolink/test_sensor.py b/tests/components/reolink/test_sensor.py index 9b32f70a9bd..9049d5906fc 100644 --- a/tests/components/reolink/test_sensor.py +++ b/tests/components/reolink/test_sensor.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_NAME, TEST_NVR_NAME from tests.common import MockConfigEntry @@ -31,10 +31,10 @@ async def test_sensors( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SENSOR}.{TEST_NVR_NAME}_ptz_pan_position" + entity_id = f"{Platform.SENSOR}.{TEST_CAM_NAME}_ptz_pan_position" assert hass.states.get(entity_id).state == "1200" - entity_id = f"{Platform.SENSOR}.{TEST_NVR_NAME}_wi_fi_signal" + entity_id = f"{Platform.SENSOR}.{TEST_CAM_NAME}_wi_fi_signal" assert hass.states.get(entity_id).state == "-55" entity_id = f"{Platform.SENSOR}.{TEST_NVR_NAME}_sd_0_storage" diff --git a/tests/components/reolink/test_siren.py b/tests/components/reolink/test_siren.py index 43156626b12..47e0e47e57f 100644 --- a/tests/components/reolink/test_siren.py +++ b/tests/components/reolink/test_siren.py @@ -22,7 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_NAME from tests.common import MockConfigEntry @@ -38,7 +38,7 @@ async def test_siren( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SIREN}.{TEST_NVR_NAME}_siren" + entity_id = f"{Platform.SIREN}.{TEST_CAM_NAME}_siren" assert hass.states.get(entity_id).state == STATE_UNKNOWN # test siren turn on @@ -98,7 +98,7 @@ async def test_siren_turn_on_errors( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SIREN}.{TEST_NVR_NAME}_siren" + entity_id = f"{Platform.SIREN}.{TEST_CAM_NAME}_siren" original = getattr(reolink_host, attr) setattr(reolink_host, attr, value) @@ -124,7 +124,7 @@ async def test_siren_turn_off_errors( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.SIREN}.{TEST_NVR_NAME}_siren" + entity_id = f"{Platform.SIREN}.{TEST_CAM_NAME}_siren" reolink_host.set_siren.side_effect = ReolinkError("Test error") with pytest.raises(HomeAssistantError): diff --git a/tests/components/reolink/test_switch.py b/tests/components/reolink/test_switch.py index 9c0f2295a20..c8a38f19d5c 100644 --- a/tests/components/reolink/test_switch.py +++ b/tests/components/reolink/test_switch.py @@ -36,7 +36,6 @@ async def test_switch( reolink_host: MagicMock, ) -> None: """Test switch entity.""" - reolink_host.camera_name.return_value = TEST_CAM_NAME reolink_host.audio_record.return_value = True with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): @@ -108,7 +107,6 @@ async def test_host_switch( reolink_host: MagicMock, ) -> None: """Test host switch entity.""" - reolink_host.camera_name.return_value = TEST_CAM_NAME reolink_host.email_enabled.return_value = True reolink_host.is_hub = False reolink_host.supported.return_value = True diff --git a/tests/components/reolink/test_update.py b/tests/components/reolink/test_update.py index d12b229e932..ce24734f9c1 100644 --- a/tests/components/reolink/test_update.py +++ b/tests/components/reolink/test_update.py @@ -34,8 +34,6 @@ async def test_no_update( entity_name: str, ) -> None: """Test update state when no update available.""" - reolink_host.camera_name.return_value = TEST_CAM_NAME - with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -53,7 +51,6 @@ async def test_update_str( entity_name: str, ) -> None: """Test update state when update available with string from API.""" - reolink_host.camera_name.return_value = TEST_CAM_NAME reolink_host.firmware_update_available.return_value = "New firmware available" with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]): @@ -75,7 +72,6 @@ async def test_update_firm( entity_name: str, ) -> None: """Test update state when update available with firmware info from reolink.com.""" - reolink_host.camera_name.return_value = TEST_CAM_NAME reolink_host.sw_upload_progress.return_value = 100 reolink_host.camera_sw_version.return_value = "v1.1.0.0.0.0000" new_firmware = NewSoftwareVersion( @@ -174,7 +170,6 @@ async def test_update_firm_keeps_available( entity_name: str, ) -> None: """Test update entity keeps being available during update.""" - reolink_host.camera_name.return_value = TEST_CAM_NAME reolink_host.camera_sw_version.return_value = "v1.1.0.0.0.0000" new_firmware = NewSoftwareVersion( version_string="v3.3.0.226_23031644", diff --git a/tests/components/reolink/test_util.py b/tests/components/reolink/test_util.py index 8b730bc708b..1ebeaf902c8 100644 --- a/tests/components/reolink/test_util.py +++ b/tests/components/reolink/test_util.py @@ -31,7 +31,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr -from .conftest import TEST_NVR_NAME, TEST_UID, TEST_UID_CAM +from .conftest import TEST_CAM_NAME, TEST_UID, TEST_UID_CAM from tests.common import MockConfigEntry @@ -115,7 +115,7 @@ async def test_try_function( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - entity_id = f"{Platform.NUMBER}.{TEST_NVR_NAME}_volume" + entity_id = f"{Platform.NUMBER}.{TEST_CAM_NAME}_volume" reolink_host.set_volume.side_effect = side_effect with pytest.raises(expected.__class__) as err: diff --git a/tests/components/rest/test_data.py b/tests/components/rest/test_data.py index 4d6bc000fac..01581c8ac68 100644 --- a/tests/components/rest/test_data.py +++ b/tests/components/rest/test_data.py @@ -1,13 +1,17 @@ """Test REST data module logging improvements.""" +from datetime import timedelta import logging +from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.rest import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -89,6 +93,59 @@ async def test_rest_data_no_warning_on_200_with_wrong_content_type( ) +async def test_rest_data_with_incorrect_charset_in_header( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that we can handle sites which provides an incorrect charset.""" + aioclient_mock.get( + "http://example.com/api", + status=200, + text="

Some html

", + headers={"Content-Type": "text/html; charset=utf-8"}, + ) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "resource": "http://example.com/api", + "method": "GET", + "encoding": "windows-1250", + "sensor": [ + { + "name": "test_sensor", + "value_template": "{{ value }}", + } + ], + } + }, + ) + await hass.async_block_till_done() + + with patch( + "tests.test_util.aiohttp.AiohttpClientMockResponse.text", + side_effect=UnicodeDecodeError("utf-8", b"", 1, 0, ""), + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + log_text = "Response charset came back as utf-8 but could not be decoded, continue with configured encoding windows-1250." + assert log_text in caplog.text + + caplog.clear() + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Only log once as we only try once with automatic decoding + assert log_text not in caplog.text + + async def test_rest_data_no_warning_on_success_json( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index 994f58513d2..6974bc5fccc 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -199,7 +199,7 @@ async def test_config_flow_failures_code_login( async def test_options_flow_drawables( - hass: HomeAssistant, mock_roborock_entry: MockConfigEntry + hass: HomeAssistant, bypass_api_fixture, mock_roborock_entry: MockConfigEntry ) -> None: """Test that the options flow works.""" with patch("homeassistant.components.roborock.roborock_storage"): diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 5d6e7a599bd..aa7da07d499 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -142,12 +142,14 @@ async def test_cloud_command( @pytest.mark.parametrize( - ("in_cleaning_int", "expected_command"), + ("in_cleaning_int", "in_returning_int", "expected_command"), [ - (0, RoborockCommand.APP_START), - (1, RoborockCommand.APP_START), - (2, RoborockCommand.RESUME_ZONED_CLEAN), - (3, RoborockCommand.RESUME_SEGMENT_CLEAN), + (0, 1, RoborockCommand.APP_CHARGE), + (0, 0, RoborockCommand.APP_START), + (1, 0, RoborockCommand.APP_START), + (2, 0, RoborockCommand.RESUME_ZONED_CLEAN), + (3, 0, RoborockCommand.RESUME_SEGMENT_CLEAN), + (4, 0, RoborockCommand.APP_RESUME_BUILD_MAP), ], ) async def test_resume_cleaning( @@ -155,11 +157,13 @@ async def test_resume_cleaning( bypass_api_fixture, mock_roborock_entry: MockConfigEntry, in_cleaning_int: int, + in_returning_int: int, expected_command: RoborockCommand, ) -> None: """Test resuming clean on start button when a clean is paused.""" prop = copy.deepcopy(PROP) prop.status.in_cleaning = in_cleaning_int + prop.status.in_returning = in_returning_int with patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", return_value=prop, diff --git a/tests/components/russound_rio/fixtures/get_zones.json b/tests/components/russound_rio/fixtures/get_zones.json index e1077944593..b51c93875f1 100644 --- a/tests/components/russound_rio/fixtures/get_zones.json +++ b/tests/components/russound_rio/fixtures/get_zones.json @@ -7,7 +7,8 @@ "volume": "10", "status": "ON", "enabled": "True", - "current_source": "1" + "current_source": "1", + "enabled_sources": [1, 2] }, "2": { "name": "Kitchen", diff --git a/tests/components/russound_rio/snapshots/test_media_browser.ambr b/tests/components/russound_rio/snapshots/test_media_browser.ambr new file mode 100644 index 00000000000..7c3df31a69b --- /dev/null +++ b/tests/components/russound_rio/snapshots/test_media_browser.ambr @@ -0,0 +1,75 @@ +# serializer version: 1 +# name: test_browse_media_root + list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': '', + 'media_content_type': 'presets', + 'thumbnail': 'https://brands.home-assistant.io/_/russound_rio/logo.png', + 'title': 'Presets', + }), + ]) +# --- +# name: test_browse_presets + list([ + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,1', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WOOD', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,2', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': '89.7 MHz FM', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,7', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WWKR', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,8', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WKLA', + }), + dict({ + 'can_expand': False, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'channel', + 'media_content_id': '1,11', + 'media_content_type': 'preset', + 'thumbnail': None, + 'title': 'WGN', + }), + ]) +# --- diff --git a/tests/components/russound_rio/test_media_browser.py b/tests/components/russound_rio/test_media_browser.py new file mode 100644 index 00000000000..d2d67e70aeb --- /dev/null +++ b/tests/components/russound_rio/test_media_browser.py @@ -0,0 +1,61 @@ +"""Tests for the Russound RIO media browser.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_integration +from .const import ENTITY_ID_ZONE_1 + +from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator + + +async def test_browse_media_root( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the root browse page.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": ENTITY_ID_ZONE_1, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["children"] == snapshot + + +async def test_browse_presets( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the presets browse page.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": ENTITY_ID_ZONE_1, + "media_content_type": "presets", + "media_content_id": "", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["children"] == snapshot diff --git a/tests/components/ruuvitag_ble/fixtures.py b/tests/components/ruuvitag_ble/fixtures.py index 5d6ac9ea470..94ed1e00331 100644 --- a/tests/components/ruuvitag_ble/fixtures.py +++ b/tests/components/ruuvitag_ble/fixtures.py @@ -12,7 +12,7 @@ NOT_RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( source="local", ) -RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( +RUUVI_V5_SERVICE_INFO = BluetoothServiceInfo( name="RuuviTag 0911", address="01:03:05:07:09:11", # Ignored (the payload encodes the correct MAC) rssi=-60, @@ -23,5 +23,16 @@ RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) +RUUVI_V6_SERVICE_INFO = BluetoothServiceInfo( + name="Ruuvi 1234", + address="01:03:05:07:12:34", # Ignored (the payload encodes the correct MAC) + rssi=-60, + manufacturer_data={ + 1177: b"\x06\x17\x0cVh\xc7\x9e\x00p\x00\xc9\x05\x01\xd9J\xcd\x00L\x88O", + }, + service_data={}, + service_uuids=[], + source="local", +) CONFIGURED_NAME = "RuuviTag EFAF" CONFIGURED_PREFIX = "ruuvitag_efaf" diff --git a/tests/components/ruuvitag_ble/snapshots/test_sensor.ambr b/tests/components/ruuvitag_ble/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..2641297bc76 --- /dev/null +++ b/tests/components/ruuvitag_ble/snapshots/test_sensor.ambr @@ -0,0 +1,1013 @@ +# serializer version: 1 +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Acceleration total', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'acceleration_total', + 'unique_id': '01:03:05:07:09:11-acceleration_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RuuviTag EFAF Acceleration total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.82', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_x-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_x', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Acceleration X', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'acceleration_x', + 'unique_id': '01:03:05:07:09:11-acceleration_x', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_x-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RuuviTag EFAF Acceleration X', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_x', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-7.02', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_y-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_y', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Acceleration Y', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'acceleration_y', + 'unique_id': '01:03:05:07:09:11-acceleration_y', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_y-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RuuviTag EFAF Acceleration Y', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_y', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.39', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_z-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_z', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Acceleration Z', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'acceleration_z', + 'unique_id': '01:03:05:07:09:11-acceleration_z', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_acceleration_z-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RuuviTag EFAF Acceleration Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_acceleration_z', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-2.51', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:09:11-humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'RuuviTag EFAF Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '61.84', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_movement_counter-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_movement_counter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Movement counter', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'movement_counter', + 'unique_id': '01:03:05:07:09:11-movement_counter', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_movement_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RuuviTag EFAF Movement counter', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_movement_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '114', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:09:11-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'RuuviTag EFAF Pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1013.54', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:09:11-signal_strength', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'RuuviTag EFAF Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-60', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:09:11-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'RuuviTag EFAF Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.2', + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_efaf_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:09:11-voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v5][sensor.ruuvitag_efaf_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'RuuviTag EFAF Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_efaf_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2395', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_carbon_dioxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_carbon_dioxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:12:34-carbon_dioxide', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_carbon_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'RuuviTag 884F Carbon dioxide', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '201', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:12:34-humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'RuuviTag 884F Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.3', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:12:34-illuminance', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'RuuviTag 884F Illuminance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13027', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_nox_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_nox_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'NOx index', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'nox_index', + 'unique_id': '01:03:05:07:12:34-nox_index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_nox_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RuuviTag 884F NOx index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_nox_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:12:34-pm25', + 'unit_of_measurement': 'μg/m³', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'RuuviTag 884F PM2.5', + 'state_class': , + 'unit_of_measurement': 'μg/m³', + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.2', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:12:34-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'RuuviTag 884F Pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1011.02', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:12:34-signal_strength', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'RuuviTag 884F Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-60', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01:03:05:07:12:34-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'RuuviTag 884F Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.5', + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_voc_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ruuvitag_884f_voc_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'VOC index', + 'platform': 'ruuvitag_ble', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voc_index', + 'unique_id': '01:03:05:07:12:34-voc_index', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[v6][sensor.ruuvitag_884f_voc_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'RuuviTag 884F VOC index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.ruuvitag_884f_voc_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- diff --git a/tests/components/ruuvitag_ble/test_config_flow.py b/tests/components/ruuvitag_ble/test_config_flow.py index 3414fa34536..5259511fc0f 100644 --- a/tests/components/ruuvitag_ble/test_config_flow.py +++ b/tests/components/ruuvitag_ble/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components.ruuvitag_ble.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .fixtures import CONFIGURED_NAME, NOT_RUUVITAG_SERVICE_INFO, RUUVITAG_SERVICE_INFO +from .fixtures import CONFIGURED_NAME, NOT_RUUVITAG_SERVICE_INFO, RUUVI_V5_SERVICE_INFO from tests.common import MockConfigEntry @@ -24,7 +24,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=RUUVITAG_SERVICE_INFO, + data=RUUVI_V5_SERVICE_INFO, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" @@ -36,7 +36,7 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME - assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address + assert result2["result"].unique_id == RUUVI_V5_SERVICE_INFO.address async def test_async_step_bluetooth_not_ruuvitag(hass: HomeAssistant) -> None: @@ -64,7 +64,7 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: """Test setup from service info cache with devices found.""" with patch( "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", - return_value=[RUUVITAG_SERVICE_INFO], + return_value=[RUUVI_V5_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -77,18 +77,18 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={"address": RUUVITAG_SERVICE_INFO.address}, + user_input={"address": RUUVI_V5_SERVICE_INFO.address}, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME - assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address + assert result2["result"].unique_id == RUUVI_V5_SERVICE_INFO.address async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", - return_value=[RUUVITAG_SERVICE_INFO], + return_value=[RUUVI_V5_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -99,7 +99,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - entry = MockConfigEntry( domain=DOMAIN, - unique_id=RUUVITAG_SERVICE_INFO.address, + unique_id=RUUVI_V5_SERVICE_INFO.address, ) entry.add_to_hass(hass) @@ -108,7 +108,7 @@ async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) - ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={"address": RUUVITAG_SERVICE_INFO.address}, + user_input={"address": RUUVI_V5_SERVICE_INFO.address}, ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -120,13 +120,13 @@ async def test_async_step_user_with_found_devices_already_setup( """Test setup from service info cache with devices found.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=RUUVITAG_SERVICE_INFO.address, + unique_id=RUUVI_V5_SERVICE_INFO.address, ) entry.add_to_hass(hass) with patch( "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", - return_value=[RUUVITAG_SERVICE_INFO], + return_value=[RUUVI_V5_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -140,14 +140,14 @@ async def test_async_step_bluetooth_devices_already_setup(hass: HomeAssistant) - """Test we can't start a flow if there is already a config entry.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=RUUVITAG_SERVICE_INFO.address, + unique_id=RUUVI_V5_SERVICE_INFO.address, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=RUUVITAG_SERVICE_INFO, + data=RUUVI_V5_SERVICE_INFO, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -158,7 +158,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=RUUVITAG_SERVICE_INFO, + data=RUUVI_V5_SERVICE_INFO, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" @@ -166,7 +166,7 @@ async def test_async_step_bluetooth_already_in_progress(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=RUUVITAG_SERVICE_INFO, + data=RUUVI_V5_SERVICE_INFO, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -179,14 +179,14 @@ async def test_async_step_user_takes_precedence_over_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=RUUVITAG_SERVICE_INFO, + data=RUUVI_V5_SERVICE_INFO, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", - return_value=[RUUVITAG_SERVICE_INFO], + return_value=[RUUVI_V5_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -199,9 +199,9 @@ async def test_async_step_user_takes_precedence_over_discovery( ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={"address": RUUVITAG_SERVICE_INFO.address}, + user_input={"address": RUUVI_V5_SERVICE_INFO.address}, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIGURED_NAME assert result2["data"] == {} - assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address + assert result2["result"].unique_id == RUUVI_V5_SERVICE_INFO.address diff --git a/tests/components/ruuvitag_ble/test_sensor.py b/tests/components/ruuvitag_ble/test_sensor.py index 14826a692a6..edeb6a4c2b5 100644 --- a/tests/components/ruuvitag_ble/test_sensor.py +++ b/tests/components/ruuvitag_ble/test_sensor.py @@ -3,47 +3,37 @@ from __future__ import annotations import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ruuvitag_ble.const import DOMAIN -from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from .fixtures import CONFIGURED_NAME, CONFIGURED_PREFIX, RUUVITAG_SERVICE_INFO +from .fixtures import RUUVI_V5_SERVICE_INFO, RUUVI_V6_SERVICE_INFO -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform from tests.components.bluetooth import inject_bluetooth_service_info -@pytest.mark.usefixtures("enable_bluetooth") -async def test_sensors(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("enable_bluetooth", "entity_registry_enabled_by_default") +@pytest.mark.parametrize( + "service_info", [RUUVI_V5_SERVICE_INFO, RUUVI_V6_SERVICE_INFO], ids=("v5", "v6") +) +async def test_sensors( + hass: HomeAssistant, + entity_registry: EntityRegistry, + snapshot: SnapshotAssertion, + service_info: BluetoothServiceInfo, +) -> None: """Test the RuuviTag BLE sensors.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=RUUVITAG_SERVICE_INFO.address) + entry = MockConfigEntry(domain=DOMAIN, unique_id=service_info.address) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 0 - inject_bluetooth_service_info( - hass, - RUUVITAG_SERVICE_INFO, - ) + inject_bluetooth_service_info(hass, service_info) await hass.async_block_till_done() - assert len(hass.states.async_all()) >= 4 - - for sensor, value, unit, state_class in ( - ("temperature", "7.2", "°C", "measurement"), - ("humidity", "61.84", "%", "measurement"), - ("pressure", "1013.54", "hPa", "measurement"), - ("voltage", "2395", "mV", "measurement"), - ): - state = hass.states.get(f"sensor.{CONFIGURED_PREFIX}_{sensor}") - assert state is not None - assert state.state == value - name_lower = state.attributes[ATTR_FRIENDLY_NAME].lower() - assert name_lower == f"{CONFIGURED_NAME} {sensor}".lower() - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit - assert state.attributes[ATTR_STATE_CLASS] == state_class + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index 6fd6314c6bb..9a9ccc0c47a 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -617,11 +617,26 @@ async def test_ws_delete( @pytest.mark.freeze_time("2022-08-10 20:10:00-07:00") @pytest.mark.parametrize( - ("to", "next_event", "saved_to"), + ("to", "next_event", "saved_to", "icon_dict"), [ - ("23:59:59", "2022-08-10T23:59:59-07:00", "23:59:59"), - ("24:00", "2022-08-11T00:00:00-07:00", "24:00:00"), - ("24:00:00", "2022-08-11T00:00:00-07:00", "24:00:00"), + ( + "23:59:59", + "2022-08-10T23:59:59-07:00", + "23:59:59", + {CONF_ICON: "mdi:party-pooper"}, + ), + ( + "24:00", + "2022-08-11T00:00:00-07:00", + "24:00:00", + {CONF_ICON: "mdi:party-popper"}, + ), + ( + "24:00:00", + "2022-08-11T00:00:00-07:00", + "24:00:00", + {}, + ), ], ) async def test_update( @@ -632,6 +647,7 @@ async def test_update( to: str, next_event: str, saved_to: str, + icon_dict: dict, ) -> None: """Test updating the schedule.""" assert await schedule_setup() @@ -654,7 +670,7 @@ async def test_update( "type": f"{DOMAIN}/update", f"{DOMAIN}_id": "from_storage", CONF_NAME: "Party pooper", - CONF_ICON: "mdi:party-pooper", + **icon_dict, CONF_MONDAY: [], CONF_TUESDAY: [], CONF_WEDNESDAY: [{CONF_FROM: "17:00:00", CONF_TO: to}], @@ -671,7 +687,7 @@ async def test_update( assert state assert state.state == STATE_ON assert state.attributes[ATTR_FRIENDLY_NAME] == "Party pooper" - assert state.attributes[ATTR_ICON] == "mdi:party-pooper" + assert state.attributes.get(ATTR_ICON) == icon_dict.get(CONF_ICON) assert state.attributes[ATTR_NEXT_EVENT].isoformat() == next_event await client.send_json({"id": 2, "type": f"{DOMAIN}/list"}) diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py index 0ffb860179d..446dbd0a0bf 100644 --- a/tests/components/select/test_device_action.py +++ b/tests/components/select/test_device_action.py @@ -376,6 +376,7 @@ async def test_get_action_capabilities( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, @@ -391,6 +392,7 @@ async def test_get_action_capabilities( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, @@ -476,6 +478,7 @@ async def test_get_action_capabilities_legacy( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, @@ -491,6 +494,7 @@ async def test_get_action_capabilities_legacy( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py index fc35757fa67..83788701877 100644 --- a/tests/components/select/test_device_condition.py +++ b/tests/components/select/test_device_condition.py @@ -276,6 +276,7 @@ async def test_get_condition_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -301,6 +302,7 @@ async def test_get_condition_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -336,6 +338,7 @@ async def test_get_condition_capabilities_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -361,6 +364,7 @@ async def test_get_condition_capabilities_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py index dbb4e23d785..1d8f8b07ad7 100644 --- a/tests/components/select/test_device_trigger.py +++ b/tests/components/select/test_device_trigger.py @@ -310,18 +310,21 @@ async def test_get_trigger_capabilities( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -341,18 +344,21 @@ async def test_get_trigger_capabilities( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -380,18 +386,21 @@ async def test_get_trigger_capabilities_unknown( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -421,18 +430,21 @@ async def test_get_trigger_capabilities_legacy( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -452,18 +464,21 @@ async def test_get_trigger_capabilities_legacy( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py index 75dbdc88840..05a4fb731d1 100644 --- a/tests/components/sensibo/test_select.py +++ b/tests/components/sensibo/test_select.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest @@ -14,16 +14,13 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.components.sensibo.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.helpers import entity_registry as er -from . import ENTRY_CONFIG - -from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +from tests.common import async_fire_time_changed, snapshot_platform @pytest.mark.parametrize( @@ -154,87 +151,3 @@ async def test_select_set_option( state = hass.states.get("select.kitchen_light") assert state.state == STATE_UNAVAILABLE - - -@pytest.mark.parametrize( - "load_platforms", - [[Platform.SELECT]], -) -async def test_deprecated_horizontal_swing_select( - hass: HomeAssistant, - load_platforms: list[Platform], - mock_client: MagicMock, - entity_registry: er.EntityRegistry, - issue_registry: ir.IssueRegistry, -) -> None: - """Test the deprecated horizontal swing select entity.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=ENTRY_CONFIG, - entry_id="1", - unique_id="firstnamelastname", - version=2, - ) - - config_entry.add_to_hass(hass) - - entity_registry.async_get_or_create( - SELECT_DOMAIN, - DOMAIN, - "ABC999111-horizontalSwing", - config_entry=config_entry, - disabled_by=None, - has_entity_name=True, - suggested_object_id="hallway_horizontal_swing", - ) - - with patch("homeassistant.components.sensibo.PLATFORMS", load_platforms): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("select.hallway_horizontal_swing") - assert state.state == "stopped" - - # No issue created without automation or script - assert issue_registry.issues == {} - - with ( - patch("homeassistant.components.sensibo.PLATFORMS", load_platforms), - patch( - # Patch check for automation, that one exist - "homeassistant.components.sensibo.select.automations_with_entity", - return_value=["automation.test"], - ), - ): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done(True) - - # Issue is created when entity is enabled and automation/script exist - issue = issue_registry.async_get_issue(DOMAIN, "deprecated_entity_horizontalswing") - assert issue - assert issue.translation_key == "deprecated_entity_horizontalswing" - assert hass.states.get("select.hallway_horizontal_swing") - assert entity_registry.async_is_registered("select.hallway_horizontal_swing") - - # Disabling the entity should remove the entity and remove the issue - # once the integration is reloaded - entity_registry.async_update_entity( - state.entity_id, disabled_by=er.RegistryEntryDisabler.USER - ) - - with ( - patch("homeassistant.components.sensibo.PLATFORMS", load_platforms), - patch( - "homeassistant.components.sensibo.select.automations_with_entity", - return_value=["automation.test"], - ), - ): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done(True) - - # Disabling the entity and reloading has removed the entity and issue - assert not hass.states.get("select.hallway_horizontal_swing") - assert not entity_registry.async_is_registered("select.hallway_horizontal_swing") - issue = issue_registry.async_get_issue(DOMAIN, "deprecated_entity_horizontalswing") - assert not issue diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index da69610f4c5..88bec54c936 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -329,12 +329,14 @@ async def test_get_condition_capabilities( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, ] @@ -398,12 +400,14 @@ async def test_get_condition_capabilities_legacy( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, ] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index c39a5216f0f..31bd0d2be55 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -277,15 +277,22 @@ async def test_get_trigger_capabilities( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] } triggers = await async_get_device_automations( @@ -347,15 +354,22 @@ async def test_get_trigger_capabilities_legacy( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] } triggers = await async_get_device_automations( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 98fb9d6604a..ce78edfe481 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -11,8 +11,12 @@ from unittest.mock import patch import pytest from homeassistant.components import sensor -from homeassistant.components.number import NumberDeviceClass +from homeassistant.components.number import ( + AMBIGUOUS_UNITS as NUMBER_AMBIGUOUS_UNITS, + NumberDeviceClass, +) from homeassistant.components.sensor import ( + AMBIGUOUS_UNITS as SENSOR_AMBIGUOUS_UNITS, DEVICE_CLASS_STATE_CLASSES, DEVICE_CLASS_UNITS, DOMAIN, @@ -159,12 +163,47 @@ async def test_temperature_conversion_wrong_device_class( assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - # Check temperature is not converted + # Check compatible unit is applied state = hass.states.get(entity0.entity_id) assert state.state == "0.0" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT +@pytest.mark.parametrize( + ("ambiguous_unit", "normalized_unit"), + [ + (ambiguous_unit, normalized_unit) + for ambiguous_unit, normalized_unit in sensor.AMBIGUOUS_UNITS.items() + ], +) +async def test_ambiguous_unit_of_measurement_compat( + hass: HomeAssistant, ambiguous_unit: str, normalized_unit: str +) -> None: + """Test ambiguous native_unit_of_measurement values are corrected.""" + entity0 = MockSensor( + name="Test", + native_value="0.0", + native_unit_of_measurement=ambiguous_unit, + ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + # Check temperature is not converted + state = hass.states.get(entity0.entity_id) + assert state.state == "0.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == normalized_unit + + +def test_ambiguous_units_of_measurement_aligned() -> None: + """Make sure all ambiguous UOM for sensor are also available for number.""" + + for ambiguous_unit, unit in SENSOR_AMBIGUOUS_UNITS.items(): + assert ambiguous_unit in NUMBER_AMBIGUOUS_UNITS + assert NUMBER_AMBIGUOUS_UNITS[ambiguous_unit] == unit + + @pytest.mark.parametrize("state_class", ["measurement", "total_increasing"]) async def test_deprecated_last_reset( hass: HomeAssistant, @@ -2958,7 +2997,6 @@ def test_device_class_units_are_complete() -> None: def test_device_class_converters_are_complete() -> None: """Test that the device class converters enum is complete.""" no_converter_device_classes = { - SensorDeviceClass.APPARENT_POWER, SensorDeviceClass.AQI, SensorDeviceClass.BATTERY, SensorDeviceClass.CO, @@ -2979,7 +3017,6 @@ def test_device_class_converters_are_complete() -> None: SensorDeviceClass.PM1, SensorDeviceClass.PM10, SensorDeviceClass.PM25, - SensorDeviceClass.REACTIVE_POWER, SensorDeviceClass.SIGNAL_STRENGTH, SensorDeviceClass.SOUND_PRESSURE, SensorDeviceClass.SULPHUR_DIOXIDE, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 43f185f939a..8b6d55cb9a9 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3755,6 +3755,44 @@ async def test_compile_hourly_statistics_convert_units_1( 30, ), (None, "m3", "m³", None, "volume", 13.050847, 13.333333, -10, 30), + (None, "\u00b5V", "\u03bcV", None, "voltage", 13.050847, 13.333333, -10, 30), + (None, "\u00b5Sv/h", "\u03bcSv/h", None, None, 13.050847, 13.333333, -10, 30), + ( + None, + "\u00b5S/cm", + "\u03bcS/cm", + None, + "conductivity", + 13.050847, + 13.333333, + -10, + 30, + ), + (None, "\u00b5g/ft³", "\u03bcg/ft³", None, None, 13.050847, 13.333333, -10, 30), + ( + None, + "\u00b5g/m³", + "\u03bcg/m³", + None, + "concentration", + 13.050847, + 13.333333, + -10, + 30, + ), + ( + None, + "\u00b5mol/s⋅m²", + "\u03bcmol/s⋅m²", + None, + None, + 13.050847, + 13.333333, + -10, + 30, + ), + (None, "\u00b5g", "\u03bcg", None, "mass", 13.050847, 13.333333, -10, 30), + (None, "\u00b5s", "\u03bcs", None, "duration", 13.050847, 13.333333, -10, 30), ], ) async def test_compile_hourly_statistics_equivalent_units_1( @@ -3884,6 +3922,17 @@ async def test_compile_hourly_statistics_equivalent_units_1( (None, "ft3", "ft³", None, 13.333333, -10, 30), (None, "ft³/m", "ft³/min", None, 13.333333, -10, 30), (None, "m3", "m³", None, 13.333333, -10, 30), + (None, "\u00b5V", "\u03bcV", None, 13.333333, -10, 30), + (SensorDeviceClass.VOLTAGE, "\u00b5V", "\u03bcV", None, 13.333333, -10, 30), + (None, "\u00b5Sv/h", "\u03bcSv/h", None, 13.333333, -10, 30), + (None, "\u00b5S/cm", "\u03bcS/cm", None, 13.333333, -10, 30), + (None, "\u00b5g/ft³", "\u03bcg/ft³", None, 13.333333, -10, 30), + (None, "\u00b5g/m³", "\u03bcg/m³", None, 13.333333, -10, 30), + (None, "\u00b5mol/s⋅m²", "\u03bcmol/s⋅m²", None, 13.333333, -10, 30), + (None, "\u00b5g", "\u03bcg", None, 13.333333, -10, 30), + (SensorDeviceClass.WEIGHT, "\u00b5g", "\u03bcg", None, 13.333333, -10, 30), + (None, "\u00b5s", "\u03bcs", None, 13.333333, -10, 30), + (SensorDeviceClass.DURATION, "\u00b5s", "\u03bcs", None, 13.333333, -10, 30), ], ) async def test_compile_hourly_statistics_equivalent_units_2( @@ -5705,6 +5754,14 @@ async def test_validate_statistics_unit_change_no_conversion( (NONE_SENSOR_ATTRIBUTES, "m3", "m³"), (NONE_SENSOR_ATTRIBUTES, "rpm", "RPM"), (NONE_SENSOR_ATTRIBUTES, "RPM", "rpm"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5V", "\u03bcV"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5Sv/h", "\u03bcSv/h"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5S/cm", "\u03bcS/cm"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5g/ft³", "\u03bcg/ft³"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5g/m³", "\u03bcg/m³"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5mol/s⋅m²", "\u03bcmol/s⋅m²"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5g", "\u03bcg"), + (NONE_SENSOR_ATTRIBUTES, "\u00b5s", "\u03bcs"), ], ) async def test_validate_statistics_unit_change_equivalent_units( @@ -5768,6 +5825,15 @@ async def test_validate_statistics_unit_change_equivalent_units( ("attributes", "unit1", "unit2", "supported_unit"), [ (NONE_SENSOR_ATTRIBUTES, "m³", "m3", "CCF, L, fl. oz., ft³, gal, mL, m³"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcV", "\u00b5V", "MV, V, kV, mV, \u03bcV"), + (NONE_SENSOR_ATTRIBUTES, "\u03bcS/cm", "\u00b5S/cm", "S/cm, mS/cm, \u03bcS/cm"), + ( + NONE_SENSOR_ATTRIBUTES, + "\u03bcg", + "\u00b5g", + "g, kg, lb, mg, oz, st, \u03bcg", + ), + (NONE_SENSOR_ATTRIBUTES, "\u03bcs", "\u00b5s", "d, h, min, ms, s, w, \u03bcs"), ], ) async def test_validate_statistics_unit_change_equivalent_units_2( diff --git a/tests/components/sleep_as_android/__init__.py b/tests/components/sleep_as_android/__init__.py new file mode 100644 index 00000000000..3b970b011e7 --- /dev/null +++ b/tests/components/sleep_as_android/__init__.py @@ -0,0 +1 @@ +"""Tests for the Sleep as Android integration.""" diff --git a/tests/components/sleep_as_android/conftest.py b/tests/components/sleep_as_android/conftest.py new file mode 100644 index 00000000000..97cc6da16a0 --- /dev/null +++ b/tests/components/sleep_as_android/conftest.py @@ -0,0 +1,34 @@ +"""Common fixtures for the Sleep as Android tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.sleep_as_android.const import DOMAIN +from homeassistant.const import CONF_WEBHOOK_ID + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.sleep_as_android.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Mock Sleep as Android configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Sleep as Android", + data={ + "cloudhook": False, + CONF_WEBHOOK_ID: "webhook_id", + }, + entry_id="01JRD840SAZ55DGXBD78PTQ4EF", + ) diff --git a/tests/components/sleep_as_android/snapshots/test_diagnostics.ambr b/tests/components/sleep_as_android/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c7e391317da --- /dev/null +++ b/tests/components/sleep_as_android/snapshots/test_diagnostics.ambr @@ -0,0 +1,8 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config_entry_data': dict({ + 'cloudhook': False, + }), + }) +# --- diff --git a/tests/components/sleep_as_android/snapshots/test_event.ambr b/tests/components/sleep_as_android/snapshots/test_event.ambr new file mode 100644 index 00000000000..27e789351a3 --- /dev/null +++ b/tests/components/sleep_as_android/snapshots/test_event.ambr @@ -0,0 +1,494 @@ +# serializer version: 1 +# name: test_setup[event.sleep_as_android_alarm_clock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'alert_dismiss', + 'alert_start', + 'rescheduled', + 'skip_next', + 'snooze_canceled', + 'snooze_clicked', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_alarm_clock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm clock', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_alarm_clock', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_alarm_clock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'alert_dismiss', + 'alert_start', + 'rescheduled', + 'skip_next', + 'snooze_canceled', + 'snooze_clicked', + ]), + 'friendly_name': 'Sleep as Android Alarm clock', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_alarm_clock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[event.sleep_as_android_lullaby-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'start', + 'stop', + 'volume_down', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_lullaby', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lullaby', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_lullaby', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_lullaby-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'start', + 'stop', + 'volume_down', + ]), + 'friendly_name': 'Sleep as Android Lullaby', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_lullaby', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[event.sleep_as_android_sleep_health-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'antisnoring', + 'apnea_alarm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_sleep_health', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sleep health', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sleep_health', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_sleep_health-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'antisnoring', + 'apnea_alarm', + ]), + 'friendly_name': 'Sleep as Android Sleep health', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_sleep_health', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[event.sleep_as_android_sleep_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'awake', + 'deep_sleep', + 'light_sleep', + 'not_awake', + 'rem', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_sleep_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sleep phase', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sleep_phase', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_sleep_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'awake', + 'deep_sleep', + 'light_sleep', + 'not_awake', + 'rem', + ]), + 'friendly_name': 'Sleep as Android Sleep phase', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_sleep_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[event.sleep_as_android_sleep_tracking-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'paused', + 'resumed', + 'started', + 'stopped', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_sleep_tracking', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sleep tracking', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sleep_tracking', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_sleep_tracking-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'paused', + 'resumed', + 'started', + 'stopped', + ]), + 'friendly_name': 'Sleep as Android Sleep tracking', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_sleep_tracking', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[event.sleep_as_android_smart_wake_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'before_smart_period', + 'smart_period', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_smart_wake_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smart wake-up', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_smart_wakeup', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_smart_wake_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'before_smart_period', + 'smart_period', + ]), + 'friendly_name': 'Sleep as Android Smart wake-up', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_smart_wake_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[event.sleep_as_android_sound_recognition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'baby', + 'cough', + 'laugh', + 'snore', + 'talk', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_sound_recognition', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound recognition', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sound_event', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_sound_recognition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'baby', + 'cough', + 'laugh', + 'snore', + 'talk', + ]), + 'friendly_name': 'Sleep as Android Sound recognition', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_sound_recognition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[event.sleep_as_android_user_notification-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'wake_up_check', + 'show_skip_next_alarm', + 'time_to_bed_alarm_alert', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.sleep_as_android_user_notification', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'User notification', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_user_notification', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[event.sleep_as_android_user_notification-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'wake_up_check', + 'show_skip_next_alarm', + 'time_to_bed_alarm_alert', + ]), + 'friendly_name': 'Sleep as Android User notification', + }), + 'context': , + 'entity_id': 'event.sleep_as_android_user_notification', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/sleep_as_android/snapshots/test_sensor.ambr b/tests/components/sleep_as_android/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fb7f7554689 --- /dev/null +++ b/tests/components/sleep_as_android/snapshots/test_sensor.ambr @@ -0,0 +1,98 @@ +# serializer version: 1 +# name: test_setup[sensor.sleep_as_android_alarm_label-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sleep_as_android_alarm_label', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm label', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_alarm_label', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.sleep_as_android_alarm_label-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sleep as Android Alarm label', + }), + 'context': , + 'entity_id': 'sensor.sleep_as_android_alarm_label', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'label', + }) +# --- +# name: test_setup[sensor.sleep_as_android_next_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sleep_as_android_next_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Next alarm', + 'platform': 'sleep_as_android', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_next_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[sensor.sleep_as_android_next_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Sleep as Android Next alarm', + }), + 'context': , + 'entity_id': 'sensor.sleep_as_android_next_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2020-02-26T12:21:00+00:00', + }) +# --- diff --git a/tests/components/sleep_as_android/test_config_flow.py b/tests/components/sleep_as_android/test_config_flow.py new file mode 100644 index 00000000000..1642263d0ed --- /dev/null +++ b/tests/components/sleep_as_android/test_config_flow.py @@ -0,0 +1,38 @@ +"""Test the Sleep as Android config flow.""" + +from unittest.mock import AsyncMock, patch + +from homeassistant.components.sleep_as_android.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + with ( + patch( + "homeassistant.components.webhook.async_generate_id", + return_value="webhook_id", + ), + patch( + "homeassistant.components.webhook.async_generate_url", + return_value="http://example.com:8123", + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Sleep as Android" + assert result["data"] == { + "cloudhook": False, + CONF_WEBHOOK_ID: "webhook_id", + } + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sleep_as_android/test_diagnostics.py b/tests/components/sleep_as_android/test_diagnostics.py new file mode 100644 index 00000000000..a3e67dafe76 --- /dev/null +++ b/tests/components/sleep_as_android/test_diagnostics.py @@ -0,0 +1,26 @@ +"""Tests for Sleep as Android diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) diff --git a/tests/components/sleep_as_android/test_event.py b/tests/components/sleep_as_android/test_event.py new file mode 100644 index 00000000000..4e3a94f919b --- /dev/null +++ b/tests/components/sleep_as_android/test_event.py @@ -0,0 +1,173 @@ +"""Test the Sleep as Android event platform.""" + +from collections.abc import Generator +from http import HTTPStatus +from unittest.mock import patch + +from freezegun.api import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform +from tests.typing import ClientSessionGenerator + + +@pytest.fixture(autouse=True) +def event_only() -> Generator[None]: + """Enable only the event platform.""" + with patch( + "homeassistant.components.sleep_as_android.PLATFORMS", + [Platform.EVENT], + ): + yield + + +@freeze_time("2025-01-01T03:30:00.000Z") +async def test_setup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Snapshot test states of event platform.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity", "payload"), + [ + ("sleep_tracking", {"event": "sleep_tracking_paused"}), + ("sleep_tracking", {"event": "sleep_tracking_resumed"}), + ("sleep_tracking", {"event": "sleep_tracking_started"}), + ("sleep_tracking", {"event": "sleep_tracking_stopped"}), + ( + "alarm_clock", + { + "event": "alarm_alert_dismiss", + "value1": "1582719660934", + "value2": "label", + }, + ), + ( + "alarm_clock", + { + "event": "alarm_alert_start", + "value1": "1582719660934", + "value2": "label", + }, + ), + ("alarm_clock", {"event": "alarm_rescheduled"}), + ( + "alarm_clock", + {"event": "alarm_skip_next", "value1": "1582719660934", "value2": "label"}, + ), + ( + "alarm_clock", + { + "event": "alarm_snooze_canceled", + "value1": "1582719660934", + "value2": "label", + }, + ), + ( + "alarm_clock", + { + "event": "alarm_snooze_clicked", + "value1": "1582719660934", + "value2": "label", + }, + ), + ("smart_wake_up", {"event": "before_smart_period", "value1": "label"}), + ("smart_wake_up", {"event": "smart_period"}), + ("sleep_health", {"event": "antisnoring"}), + ("sleep_health", {"event": "apnea_alarm"}), + ("lullaby", {"event": "lullaby_start"}), + ("lullaby", {"event": "lullaby_stop"}), + ("lullaby", {"event": "lullaby_volume_down"}), + ("sleep_phase", {"event": "awake"}), + ("sleep_phase", {"event": "deep_sleep"}), + ("sleep_phase", {"event": "light_sleep"}), + ("sleep_phase", {"event": "not_awake"}), + ("sleep_phase", {"event": "rem"}), + ("sound_recognition", {"event": "sound_event_baby"}), + ("sound_recognition", {"event": "sound_event_cough"}), + ("sound_recognition", {"event": "sound_event_laugh"}), + ("sound_recognition", {"event": "sound_event_snore"}), + ("sound_recognition", {"event": "sound_event_talk"}), + ("user_notification", {"event": "alarm_wake_up_check"}), + ( + "user_notification", + { + "event": "show_skip_next_alarm", + "value1": "1582719660934", + "value2": "label", + }, + ), + ( + "user_notification", + { + "event": "time_to_bed_alarm_alert", + "value1": "1582719660934", + "value2": "label", + }, + ), + ], +) +@freeze_time("2025-01-01T03:30:00.000+00:00") +async def test_webhook_event( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + entity: str, + payload: dict[str, str], +) -> None: + """Test webhook events.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert (state := hass.states.get(f"event.sleep_as_android_{entity}")) + assert state.state == STATE_UNKNOWN + + client = await hass_client_no_auth() + + response = await client.post("/api/webhook/webhook_id", json=payload) + assert response.status == HTTPStatus.NO_CONTENT + + assert (state := hass.states.get(f"event.sleep_as_android_{entity}")) + assert state.state == "2025-01-01T03:30:00.000+00:00" + + +async def test_webhook_invalid( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, +) -> None: + """Test webhook event call with invalid data.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + client = await hass_client_no_auth() + + response = await client.post("/api/webhook/webhook_id", json={}) + + assert response.status == HTTPStatus.UNPROCESSABLE_ENTITY diff --git a/tests/components/sleep_as_android/test_init.py b/tests/components/sleep_as_android/test_init.py new file mode 100644 index 00000000000..27177a5a5ad --- /dev/null +++ b/tests/components/sleep_as_android/test_init.py @@ -0,0 +1,22 @@ +"""Test the Sleep as Android integration setup.""" + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_entry_setup_unload( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test integration setup and unload.""" + + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(config_entry.entry_id) + + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/sleep_as_android/test_sensor.py b/tests/components/sleep_as_android/test_sensor.py new file mode 100644 index 00000000000..760df1e0181 --- /dev/null +++ b/tests/components/sleep_as_android/test_sensor.py @@ -0,0 +1,124 @@ +"""Test the Sleep as Android sensor platform.""" + +from collections.abc import Generator +from datetime import datetime +from http import HTTPStatus +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import entity_registry as er + +from tests.common import ( + MockConfigEntry, + mock_restore_cache_with_extra_data, + snapshot_platform, +) +from tests.typing import ClientSessionGenerator + + +@pytest.fixture(autouse=True) +def sensor_only() -> Generator[None]: + """Enable only the sensor platform.""" + with patch( + "homeassistant.components.sleep_as_android.PLATFORMS", + [Platform.SENSOR], + ): + yield + + +async def test_setup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Snapshot test states of sensor platform.""" + + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.sleep_as_android_next_alarm", + "", + ), + { + "native_value": datetime.fromisoformat("2020-02-26T12:21:00+00:00"), + "native_unit_of_measurement": None, + }, + ), + ( + State( + "sensor.sleep_as_android_alarm_label", + "", + ), + { + "native_value": "label", + "native_unit_of_measurement": None, + }, + ), + ), + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + "event", + [ + "alarm_snooze_clicked", + "alarm_snooze_canceled", + "alarm_alert_start", + "alarm_alert_dismiss", + "alarm_skip_next", + "show_skip_next_alarm", + "alarm_rescheduled", + ], +) +async def test_webhook_sensor( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + event: str, +) -> None: + """Test webhook updates sensor.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert (state := hass.states.get("sensor.sleep_as_android_next_alarm")) + assert state.state == STATE_UNKNOWN + + assert (state := hass.states.get("sensor.sleep_as_android_alarm_label")) + assert state.state == STATE_UNKNOWN + + client = await hass_client_no_auth() + + response = await client.post( + "/api/webhook/webhook_id", + json={ + "event": event, + "value1": "1582719660934", + "value2": "label", + }, + ) + assert response.status == HTTPStatus.NO_CONTENT + + assert (state := hass.states.get("sensor.sleep_as_android_next_alarm")) + assert state.state == "2020-02-26T12:21:00+00:00" + + assert (state := hass.states.get("sensor.sleep_as_android_alarm_label")) + assert state.state == "label" diff --git a/tests/components/smarla/conftest.py b/tests/components/smarla/conftest.py index d472e929bcc..d25dab2446f 100644 --- a/tests/components/smarla/conftest.py +++ b/tests/components/smarla/conftest.py @@ -73,8 +73,20 @@ def mock_federwiege(mock_connection: MagicMock) -> Generator[MagicMock]: mock_babywiege_service.props["smart_mode"].get.return_value = False mock_babywiege_service.props["intensity"].get.return_value = 1 + mock_analyser_service = MagicMock(spec=Service) + mock_analyser_service.props = { + "oscillation": MagicMock(spec=Property), + "activity": MagicMock(spec=Property), + "swing_count": MagicMock(spec=Property), + } + + mock_analyser_service.props["oscillation"].get.return_value = [0, 0] + mock_analyser_service.props["activity"].get.return_value = 0 + mock_analyser_service.props["swing_count"].get.return_value = 0 + federwiege.services = { "babywiege": mock_babywiege_service, + "analyser": mock_analyser_service, } federwiege.get_property = MagicMock( diff --git a/tests/components/smarla/snapshots/test_sensor.ambr b/tests/components/smarla/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..88d6a6ecea6 --- /dev/null +++ b/tests/components/smarla/snapshots/test_sensor.ambr @@ -0,0 +1,208 @@ +# serializer version: 1 +# name: test_entities[sensor.smarla_activity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smarla_activity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Activity', + 'platform': 'smarla', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'activity', + 'unique_id': 'ABCD-activity', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.smarla_activity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smarla Activity', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.smarla_activity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entities[sensor.smarla_amplitude-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smarla_amplitude', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Amplitude', + 'platform': 'smarla', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'amplitude', + 'unique_id': 'ABCD-amplitude', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.smarla_amplitude-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smarla Amplitude', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smarla_amplitude', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entities[sensor.smarla_period-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smarla_period', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Period', + 'platform': 'smarla', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'period', + 'unique_id': 'ABCD-period', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.smarla_period-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smarla Period', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smarla_period', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entities[sensor.smarla_swing_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smarla_swing_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Swing count', + 'platform': 'smarla', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'swing_count', + 'unique_id': 'ABCD-swing_count', + 'unit_of_measurement': 'swings', + }) +# --- +# name: test_entities[sensor.smarla_swing_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smarla Swing count', + 'state_class': , + 'unit_of_measurement': 'swings', + }), + 'context': , + 'entity_id': 'sensor.smarla_swing_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/smarla/test_sensor.py b/tests/components/smarla/test_sensor.py new file mode 100644 index 00000000000..196e6d2a6f0 --- /dev/null +++ b/tests/components/smarla/test_sensor.py @@ -0,0 +1,85 @@ +"""Test sensor platform for Swing2Sleep Smarla integration.""" + +from unittest.mock import MagicMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration, update_property_listeners + +from tests.common import MockConfigEntry, snapshot_platform + +SENSOR_ENTITIES = [ + { + "entity_id": "sensor.smarla_amplitude", + "service": "analyser", + "property": "oscillation", + "test_value": [1, 0], + }, + { + "entity_id": "sensor.smarla_period", + "service": "analyser", + "property": "oscillation", + "test_value": [0, 1], + }, + { + "entity_id": "sensor.smarla_activity", + "service": "analyser", + "property": "activity", + "test_value": 1, + }, + { + "entity_id": "sensor.smarla_swing_count", + "service": "analyser", + "property": "swing_count", + "test_value": 1, + }, +] + + +@pytest.mark.usefixtures("mock_federwiege") +async def test_entities( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the Smarla entities.""" + with ( + patch("homeassistant.components.smarla.PLATFORMS", [Platform.SENSOR]), + ): + assert await setup_integration(hass, mock_config_entry) + + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.parametrize("entity_info", SENSOR_ENTITIES) +async def test_sensor_state_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_federwiege: MagicMock, + entity_info: dict[str, str], +) -> None: + """Test Smarla Sensor callback.""" + assert await setup_integration(hass, mock_config_entry) + + mock_sensor_property = mock_federwiege.get_property( + entity_info["service"], entity_info["property"] + ) + + entity_id = entity_info["entity_id"] + + assert hass.states.get(entity_id).state == "0" + + mock_sensor_property.get.return_value = entity_info["test_value"] + + await update_property_listeners(mock_sensor_property) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "1" diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 93f505872f4..f13617d64d5 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -96,6 +96,7 @@ def mock_smartthings() -> Generator[AsyncMock]: @pytest.fixture( params=[ + "aq_sensor_3_ikea", "da_ac_airsensor_01001", "da_ac_rac_000001", "da_ac_rac_000003", @@ -112,6 +113,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "centralite", "da_ref_normal_000001", "da_ref_normal_01011", + "da_ref_normal_01011_onedoor", "da_ref_normal_01001", "vd_network_audio_002s", "vd_network_audio_003s", diff --git a/tests/components/smartthings/fixtures/device_status/aq_sensor_3_ikea.json b/tests/components/smartthings/fixtures/device_status/aq_sensor_3_ikea.json new file mode 100644 index 00000000000..383c5d1e85e --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/aq_sensor_3_ikea.json @@ -0,0 +1,94 @@ +{ + "components": { + "main": { + "tvocMeasurement": { + "tvocLevel": { + "value": 0.1, + "unit": "ppm", + "timestamp": "2025-08-15T13:48:52.222Z" + } + }, + "fineDustSensor": { + "fineDustLevel": { + "value": 1, + "unit": "\u03bcg/m^3", + "timestamp": "2025-08-15T13:29:36.938Z" + } + }, + "relativeHumidityMeasurement": { + "humidity": { + "value": 53.0, + "unit": "%", + "timestamp": "2025-08-15T13:48:42.554Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": { + "minimum": -10.0, + "maximum": 50.0 + }, + "unit": "C", + "timestamp": "2025-03-19T19:48:47.896Z" + }, + "temperature": { + "value": 22.0, + "unit": "C", + "timestamp": "2025-08-15T12:25:37.127Z" + } + }, + "refresh": {}, + "airQualityHealthConcern": { + "supportedAirQualityValues": { + "value": null + }, + "airQualityHealthConcern": { + "value": "good", + "timestamp": "2025-08-15T13:17:38.791Z" + } + }, + "firmwareUpdate": { + "lastUpdateStatusReason": { + "value": "TOO_MANY_CLIENTS", + "timestamp": "2025-06-09T05:59:52.076Z" + }, + "imageTransferProgress": { + "value": null + }, + "availableVersion": { + "value": "00010010", + "timestamp": "2025-03-19T19:49:07.016Z" + }, + "lastUpdateStatus": { + "value": "updateFailed", + "timestamp": "2025-06-09T05:59:52.072Z" + }, + "supportedCommands": { + "value": null + }, + "state": { + "value": "normalOperation", + "timestamp": "2025-06-09T05:59:52.105Z" + }, + "estimatedTimeRemaining": { + "value": null + }, + "updateAvailable": { + "value": false, + "timestamp": "2025-03-19T19:49:07.014Z" + }, + "currentVersion": { + "value": "00010010", + "timestamp": "2025-06-09T05:59:52.071Z" + }, + "lastUpdateTime": { + "value": "2025-06-09T05:59:51Z", + "timestamp": "2025-06-09T05:59:52.076Z" + }, + "supportsProgressReports": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/device_status/da_ref_normal_01011_onedoor.json b/tests/components/smartthings/fixtures/device_status/da_ref_normal_01011_onedoor.json new file mode 100644 index 00000000000..5cb33eb9535 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_ref_normal_01011_onedoor.json @@ -0,0 +1,1380 @@ +{ + "components": { + "main": { + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-08-14T05:31:51.945Z" + } + }, + "samsungce.fridgeWelcomeLighting": { + "detectionProximity": { + "value": null + }, + "supportedDetectionProximities": { + "value": null + }, + "status": { + "value": null + } + }, + "samsungce.viewInside": { + "supportedFocusAreas": { + "value": null + }, + "contents": { + "value": null + }, + "lastUpdatedTime": { + "value": null + } + }, + "samsungce.deviceIdentification": { + "micomAssayCode": { + "value": "00130445", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "modelName": { + "value": null + }, + "serialNumber": { + "value": null + }, + "serialNumberExtra": { + "value": null + }, + "modelClassificationCode": { + "value": "00090026001610304100000021010000", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "description": { + "value": "TP1X_REF_21K", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "releaseYear": { + "value": null + }, + "binaryId": { + "value": "TP1X_REF_21K", + "timestamp": "2025-08-14T03:17:25.761Z" + } + }, + "samsungce.quickControl": { + "version": { + "value": "1.0", + "timestamp": "2025-08-12T13:08:24.409Z" + } + }, + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "ocf": { + "st": { + "value": null + }, + "mndt": { + "value": null + }, + "mnfv": { + "value": "A-RFWW-TP1-24-T4-COM_20250706", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "mnhw": { + "value": "Realtek", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "di": { + "value": "271d82e9-5b0c-e4b8-058e-cdf23a188610", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "mnsl": { + "value": "http://www.samsung.com", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "dmv": { + "value": "1.2.1", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "n": { + "value": "Samsung-Refrigerator", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "mnmo": { + "value": "TP1X_REF_21K|00130445|00090026001610304100000021010000", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "vid": { + "value": "DA-REF-NORMAL-01011", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "mnml": { + "value": "http://www.samsung.com", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "mnpv": { + "value": "SYSTEM 2.0", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "mnos": { + "value": "TizenRT 4.0", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "pi": { + "value": "271d82e9-5b0c-e4b8-058e-cdf23a188610", + "timestamp": "2025-08-12T13:21:14.953Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2025-08-12T13:21:14.953Z" + } + }, + "samsungce.fridgeVacationMode": { + "vacationMode": { + "value": "off", + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "samsungce.driverState": { + "driverState": { + "value": { + "device/0": [ + { + "rt": ["x.com.samsung.devcol", "oic.wk.col"], + "if": ["oic.if.baseline", "oic.if.ll", "oic.if.b"] + }, + { + "href": "/alarms/vs/0", + "rep": { + "href": "/alarms/vs/0" + } + }, + { + "href": "/bespoke/vs/0", + "rep": { + "x.com.samsung.da.BespokeProduct": "On", + "href": "/bespoke/vs/0" + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "", + "x.com.samsung.da.countryCode": "", + "href": "/configuration/vs/0" + } + }, + { + "href": "/door/onedoorfreezer/vs/0", + "rep": { + "x.com.samsung.da.openState": "Close", + "href": "/door/onedoorfreezer/vs/0" + } + }, + { + "href": "/doors/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.openState": "Close", + "x.com.samsung.da.id": "3", + "x.com.samsung.da.description": "Door" + } + ], + "href": "/doors/vs/0" + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.drlcLevel": "2", + "x.com.samsung.da.override": "Not_Supported", + "x.com.samsung.da.durationminutes": "1129", + "x.com.samsung.da.start": "2025-08-13T12:34:56Z", + "x.com.samsung.da.realSaving": "On", + "href": "/drlc/vs/0" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativeConsumption": "263", + "x.com.samsung.da.instantaneousPower": "1", + "x.com.samsung.da.cumulativePower": "801", + "x.com.samsung.da.cumulativeSavedPower": "30", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "href": "/energy/consumption/vs/0" + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+02:00", + "x.com.samsung.supprtedtype": 1, + "href": "/file/information/vs/0" + } + }, + { + "href": "/refrigeration/vs/0", + "rep": { + "x.com.samsung.da.rapidFridge": "Off", + "href": "/refrigeration/vs/0" + } + }, + { + "href": "/energy/ailevel/vs/0", + "rep": { + "aiLevel": "1", + "supportedAiLevel": ["1"], + "href": "/energy/ailevel/vs/0" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "TP1X_REF_21K|00130445|00090026001610304100000021010000", + "x.com.samsung.da.description": "TP1X_REF_21K", + "x.com.samsung.da.serialNum": "08174EAY700355P", + "x.com.samsung.da.otnDUID": "EXCCN6NY7KZ4W", + "x.com.samsung.da.diagDumpType": "file", + "x.com.samsung.da.diagEndPoint": "SSM", + "x.com.samsung.da.diagLogType": ["errCode", "dump"], + "x.com.samsung.da.diagMnid": "0AJT", + "x.com.samsung.da.diagSetupid": "RO0", + "x.com.samsung.da.diagProtocolType": "WIFI_HTTPS", + "x.com.samsung.da.diagMinVersion": "1.0", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "WiFi Module", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "250706", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Micom", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "2408090D, FFFFFFFF", + "x.com.samsung.da.newVersionAvailable": "0" + } + ], + "href": "/information/vs/0" + } + }, + { + "href": "/status/lock/vs/0", + "rep": { + "x.com.samsung.da.childlock": "Locked", + "href": "/status/lock/vs/0" + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.modes": ["RVACATION_OFF"], + "x.com.samsung.da.supportedModes": [ + "HOMECARE_WIZARD_V2", + "ENERGY_REPORT_MODEL", + "18K_REF_OUTDOOR_CONTROL_V2" + ], + "href": "/mode/vs/0" + } + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.periodicnotisubscription": "true", + "href": "/realtimenotiforclient/vs/0" + } + }, + { + "href": "/runningmode/vs/0", + "rep": { + "x.com.samsung.da.runningMode": 0, + "href": "/runningmode/vs/0" + } + }, + { + "href": "/selfcheck/vs/0", + "rep": { + "x.com.samsung.da.supportedActions": ["Start"], + "x.com.samsung.da.status": "Ready", + "x.com.samsung.da.result": "Success", + "x.com.samsung.da.error": ["ErrorCode_None"], + "href": "/selfcheck/vs/0" + } + }, + { + "href": "/temperature/desired/cooler/0", + "rep": { + "temperature": 3.0, + "range": [1.0, 7.0], + "units": "C", + "href": "/temperature/desired/cooler/0" + } + }, + { + "href": "/temperature/current/cooler/0", + "rep": { + "temperature": 3.0, + "range": [1.0, 7.0], + "units": "C", + "href": "/temperature/current/cooler/0" + } + }, + { + "href": "/temperatures/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Fridge", + "x.com.samsung.da.desired": "3", + "x.com.samsung.da.current": "3", + "x.com.samsung.da.maximum": "7", + "x.com.samsung.da.minimum": "1", + "x.com.samsung.da.unit": "Celsius" + } + ], + "href": "/temperatures/vs/0" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false", + "otnStatus": "None", + "flashingProgress": "0", + "otnCompleteDate": "noHistory", + "scheduledTime": "None", + "swVersionInfo": { + "platform": "Tizen Lite", + "oneUiVersion": "7.0 Refrigerator", + "osVersion": "4.0" + }, + "otnList": [ + { + "type": "WIFI", + "modelId": "A-RFWW-TP1-24-T4-COM", + "versions": ["20250706"], + "visVersion": "250706" + }, + { + "type": "Micom", + "modelId": "028100130445FFFFFFFF", + "versions": ["2408090D", "FFFFFFFF"], + "visVersion": "240809" + } + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Europe/Warsaw", + "offset": "+02:00", + "DST": "ON" + } + }, + { + "href": "/connectionconfig/vs/0", + "rep": { + "autoReconnectionMinVersion": "1.0", + "autoReconnection": "true", + "autoReconnectionProtocolType": ["helper_hotspot", "ble_ocf"], + "supportedWiFiAuthType": [ + "OPEN", + "WEP", + "WPA-PSK", + "WPA2-PSK", + "SAE" + ], + "supportedWiFiCryptoType": [ + "TKIP", + "AES", + "WEP-64", + "WEP-128" + ], + "supportedWiFiFreq": ["2.4G"], + "calmConnectionCare": { + "version": "1.0", + "role": ["things"] + } + } + }, + { + "href": "/wirelessinfo/vs/0", + "rep": { + "macaddressWiFi": "34:FC:99:0A:67:55", + "macaddressBLE": "34:FC:99:0A:67:56" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + }, + { + "href": "/dginformation/vs/0", + "rep": { + "enrolmentstatus": "Unknown", + "devicestate": "Unknown", + "lockstatus": "Unknown", + "nextduedate": "", + "workingminutes": 0, + "paymentinfo": { + "emiplan": "Unknown", + "currency": "Unknown", + "totalemi": 0, + "totalemipaid": 0 + } + } + } + ] + }, + "timestamp": "2025-08-14T03:17:25.764Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "temperatureMeasurement", + "thermostatCoolingSetpoint", + "custom.fridgeMode", + "custom.deodorFilter", + "custom.waterFilter", + "custom.dustFilter", + "samsungce.viewInside", + "samsungce.fridgeWelcomeLighting", + "sec.smartthingsHub", + "samsungce.powerFreeze", + "samsungce.sabbathMode" + ], + "timestamp": "2025-08-13T17:29:03.375Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 25060101, + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "sec.diagnosticsInformation": { + "logType": { + "value": ["errCode", "dump"], + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "endpoint": { + "value": "SSM", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "minVersion": { + "value": "1.0", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "signinPermission": { + "value": null + }, + "setupId": { + "value": "RO0", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "protocolType": { + "value": "wifi_https", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "tsId": { + "value": null + }, + "mnId": { + "value": "0AJT", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "dumpType": { + "value": "file", + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "custom.deviceReportStateConfiguration": { + "reportStateRealtimePeriod": { + "value": null + }, + "reportStateRealtime": { + "value": { + "state": "disabled" + }, + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "reportStatePeriod": { + "value": "enabled", + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + }, + "custom.disabledComponents": { + "disabledComponents": { + "value": [ + "icemaker", + "icemaker-02", + "icemaker-03", + "pantry-01", + "pantry-02", + "specialzone-01", + "scale-10", + "scale-11", + "cooler", + "freezer", + "cvroom" + ], + "timestamp": "2025-08-12T12:36:05.791Z" + } + }, + "demandResponseLoadControl": { + "drlcStatus": { + "value": { + "drlcType": 1, + "drlcLevel": 2, + "start": "2025-08-14T07:24:20Z", + "duration": 1441, + "override": false + }, + "timestamp": "2025-08-14T07:24:23.569Z" + } + }, + "samsungce.sabbathMode": { + "supportedActions": { + "value": null + }, + "status": { + "value": null + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "energy": 861, + "deltaEnergy": 0, + "power": 1, + "powerEnergy": 0.27936416665712993, + "persistedEnergy": 0, + "energySaved": 0, + "persistedSavedEnergy": 35, + "start": "2025-08-14T07:04:50Z", + "end": "2025-08-14T07:21:35Z" + }, + "timestamp": "2025-08-14T07:21:35.717Z" + } + }, + "refresh": {}, + "execute": { + "data": { + "value": null + } + }, + "sec.wifiConfiguration": { + "autoReconnection": { + "value": true, + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "minVersion": { + "value": "1.0", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "supportedWiFiFreq": { + "value": ["2.4G"], + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "supportedAuthType": { + "value": ["OPEN", "WEP", "WPA-PSK", "WPA2-PSK", "SAE"], + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "protocolType": { + "value": ["helper_hotspot", "ble_ocf"], + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "samsungce.selfCheck": { + "result": { + "value": "passed", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "supportedActions": { + "value": ["start"], + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "progress": { + "value": null + }, + "errors": { + "value": [], + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "status": { + "value": "ready", + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "samsungce.softwareVersion": { + "versions": { + "value": [ + { + "id": "0", + "swType": "Software", + "versionNumber": "250706", + "description": "WiFi Module" + }, + { + "id": "1", + "swType": "Firmware", + "versionNumber": "2408090D, FFFFFFFF", + "description": "Micom" + } + ], + "timestamp": "2025-08-12T13:21:17.127Z" + } + }, + "custom.dustFilter": { + "dustFilterUsageStep": { + "value": null + }, + "dustFilterUsage": { + "value": null + }, + "dustFilterLastResetDate": { + "value": null + }, + "dustFilterStatus": { + "value": null + }, + "dustFilterCapacity": { + "value": null + }, + "dustFilterResetType": { + "value": null + } + }, + "refrigeration": { + "defrost": { + "value": null + }, + "rapidCooling": { + "value": "off", + "timestamp": "2025-08-12T14:27:24.223Z" + }, + "rapidFreezing": { + "value": null + } + }, + "custom.deodorFilter": { + "deodorFilterCapacity": { + "value": null + }, + "deodorFilterLastResetDate": { + "value": null + }, + "deodorFilterStatus": { + "value": null + }, + "deodorFilterResetType": { + "value": null + }, + "deodorFilterUsage": { + "value": null + }, + "deodorFilterUsageStep": { + "value": null + } + }, + "samsungce.powerCool": { + "activated": { + "value": false, + "timestamp": "2025-08-12T14:27:24.223Z" + } + }, + "custom.energyType": { + "energyType": { + "value": "2.0", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "energySavingSupport": { + "value": true, + "timestamp": "2025-08-13T12:50:26.756Z" + }, + "drMaxDuration": { + "value": 99999999, + "unit": "min", + "timestamp": "2025-08-12T12:36:06.104Z" + }, + "energySavingLevel": { + "value": 1, + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "energySavingInfo": { + "value": null + }, + "supportedEnergySavingLevels": { + "value": [1], + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "energySavingOperation": { + "value": true, + "timestamp": "2025-08-14T07:24:28.808Z" + }, + "notificationTemplateID": { + "value": null + }, + "energySavingOperationSupport": { + "value": true, + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "samsungce.softwareUpdate": { + "targetModule": { + "value": {}, + "timestamp": "2025-08-12T13:14:52.642Z" + }, + "otnDUID": { + "value": "EXCCN6NY7KZ4W", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "lastUpdatedDate": { + "value": null + }, + "availableModules": { + "value": [], + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "newVersionAvailable": { + "value": false, + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "operatingState": { + "value": "none", + "timestamp": "2025-08-12T13:14:52.642Z" + }, + "progress": { + "value": 0, + "unit": "%", + "timestamp": "2025-08-12T13:08:24.243Z" + } + }, + "samsungce.powerFreeze": { + "activated": { + "value": null + } + }, + "sec.smartthingsHub": { + "threadHardwareAvailability": { + "value": null + }, + "availability": { + "value": null + }, + "deviceId": { + "value": null + }, + "zigbeeHardwareAvailability": { + "value": null + }, + "version": { + "value": null + }, + "threadRequiresExternalHardware": { + "value": null + }, + "zigbeeRequiresExternalHardware": { + "value": null + }, + "eui": { + "value": null + }, + "lastOnboardingResult": { + "value": null + }, + "zwaveHardwareAvailability": { + "value": null + }, + "zwaveRequiresExternalHardware": { + "value": null + }, + "state": { + "value": null + }, + "onboardingProgress": { + "value": null + }, + "lastOnboardingErrorCode": { + "value": null + } + }, + "custom.waterFilter": { + "waterFilterUsageStep": { + "value": null + }, + "waterFilterResetType": { + "value": null + }, + "waterFilterCapacity": { + "value": null + }, + "waterFilterLastResetDate": { + "value": null + }, + "waterFilterUsage": { + "value": null + }, + "waterFilterStatus": { + "value": null + } + } + }, + "cvroom": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + } + }, + "specialzone-01": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + } + }, + "pantry-01": { + "samsungce.foodDefrost": { + "supportedOptions": { + "value": null + }, + "foodType": { + "value": null + }, + "weight": { + "value": null + }, + "operationTime": { + "value": null + }, + "remainingTime": { + "value": null + } + }, + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "samsungce.fridgePantryInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.meatAging": { + "zoneInfo": { + "value": null + }, + "supportedMeatTypes": { + "value": null + }, + "supportedAgingMethods": { + "value": null + }, + "status": { + "value": null + } + }, + "samsungce.fridgePantryMode": { + "mode": { + "value": null + }, + "supportedModes": { + "value": null + } + } + }, + "pantry-02": { + "samsungce.foodDefrost": { + "supportedOptions": { + "value": null + }, + "foodType": { + "value": null + }, + "weight": { + "value": null + }, + "operationTime": { + "value": null + }, + "remainingTime": { + "value": null + } + }, + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "samsungce.fridgePantryInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.meatAging": { + "zoneInfo": { + "value": null + }, + "supportedMeatTypes": { + "value": null + }, + "supportedAgingMethods": { + "value": null + }, + "status": { + "value": null + } + }, + "samsungce.fridgePantryMode": { + "mode": { + "value": null + }, + "supportedModes": { + "value": null + } + } + }, + "icemaker": { + "samsungce.fridgeIcemakerInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "switch": { + "switch": { + "value": null + } + } + }, + "onedoor": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": "closed", + "timestamp": "2025-08-14T05:31:51.945Z" + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.freezerConvertMode", "custom.fridgeMode"], + "timestamp": "2025-08-12T12:36:06.177Z" + } + }, + "samsungce.temperatureSetting": { + "supportedDesiredTemperatures": { + "value": null + }, + "desiredTemperature": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 3, + "unit": "C", + "timestamp": "2025-08-13T19:45:46.907Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 1, + "unit": "C", + "timestamp": "2025-08-12T12:36:06.031Z" + }, + "maximumSetpoint": { + "value": 7, + "unit": "C", + "timestamp": "2025-08-12T12:36:06.031Z" + } + }, + "samsungce.freezerConvertMode": { + "supportedFreezerConvertModes": { + "value": null + }, + "freezerConvertMode": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": { + "minimum": 1, + "maximum": 7, + "step": 1 + }, + "unit": "C", + "timestamp": "2025-08-12T12:36:06.031Z" + }, + "coolingSetpoint": { + "value": 3, + "unit": "C", + "timestamp": "2025-08-13T19:43:49.744Z" + } + } + }, + "scale-10": { + "samsungce.connectionState": { + "connectionState": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.weightMeasurement": { + "weight": { + "value": null + } + }, + "samsungce.weightMeasurementCalibration": {} + }, + "scale-11": { + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "samsungce.weightMeasurement": { + "weight": { + "value": null + } + } + }, + "cooler": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": null + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.temperatureSetting", "custom.fridgeMode"], + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "samsungce.temperatureSetting": { + "supportedDesiredTemperatures": { + "value": null + }, + "desiredTemperature": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 21, + "unit": "C", + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 1, + "unit": "C", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "maximumSetpoint": { + "value": 7, + "unit": "C", + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": { + "minimum": 1, + "maximum": 7, + "step": 1 + }, + "unit": "C", + "timestamp": "2025-08-12T12:36:03.771Z" + }, + "coolingSetpoint": { + "value": 3, + "unit": "C", + "timestamp": "2025-08-12T12:36:03.771Z" + } + } + }, + "freezer": { + "custom.fridgeMode": { + "fridgeModeValue": { + "value": null + }, + "fridgeMode": { + "value": null + }, + "supportedFullFridgeModes": { + "value": null + }, + "supportedFridgeModes": { + "value": null + } + }, + "contactSensor": { + "contact": { + "value": null + } + }, + "samsungce.unavailableCapabilities": { + "unavailableCommands": { + "value": [], + "timestamp": "2025-08-12T12:36:03.771Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["custom.fridgeMode", "samsungce.freezerConvertMode"], + "timestamp": "2025-08-12T12:42:30.879Z" + } + }, + "samsungce.temperatureSetting": { + "supportedDesiredTemperatures": { + "value": null + }, + "desiredTemperature": { + "value": null + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": null + }, + "maximumSetpoint": { + "value": null + } + }, + "samsungce.freezerConvertMode": { + "supportedFreezerConvertModes": { + "value": null + }, + "freezerConvertMode": { + "value": null + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + } + }, + "icemaker-02": { + "samsungce.fridgeIcemakerInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "switch": { + "switch": { + "value": null + } + } + }, + "icemaker-03": { + "samsungce.fridgeIcemakerInfo": { + "name": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": null + } + }, + "switch": { + "switch": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/aq_sensor_3_ikea.json b/tests/components/smartthings/fixtures/devices/aq_sensor_3_ikea.json new file mode 100644 index 00000000000..dc4c4821587 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/aq_sensor_3_ikea.json @@ -0,0 +1,77 @@ +{ + "items": [ + { + "deviceId": "e44d4e5c-45ea-498f-a653-f5d0c3d97bb8", + "name": "humidity-temp-dust-tvoc", + "label": "aq-sensor-3-ikea", + "manufacturerName": "SmartThingsCommunity", + "presentationId": "a39f5e57-c861-3904-9567-acda80b7cf2d", + "deviceManufacturerCode": "IKEA of Sweden", + "locationId": "9fbc89a0-5c32-494d-9a49-68186d6a5387", + "ownerId": "d051c4c5-8ccb-47b9-87ee-7ebb99694b9f", + "roomId": "f5ce3177-e0a6-4415-8496-bafa1611ee62", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "relativeHumidityMeasurement", + "version": 1 + }, + { + "id": "fineDustSensor", + "version": 1 + }, + { + "id": "airQualityHealthConcern", + "version": 1 + }, + { + "id": "tvocMeasurement", + "version": 1 + }, + { + "id": "firmwareUpdate", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + } + ], + "categories": [ + { + "name": "AirQualityDetector", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2025-03-19T19:48:39.571Z", + "parentDeviceId": "5616f552-5bae-4a1f-94b3-9eb2673a1b28", + "profile": { + "id": "0720a973-6923-39d4-8991-3aaed6edf5d5" + }, + "zigbee": { + "eui": "0CAE5FFFFECE4328", + "networkId": "846B", + "driverId": "9bee78b3-204e-4118-8265-8767f9152c49", + "executingLocally": true, + "hubId": "5616f552-5bae-4a1f-94b3-9eb2673a1b28", + "provisioningState": "PROVISIONED" + }, + "type": "ZIGBEE", + "restrictionTier": 0, + "allowed": null, + "executionContext": "LOCAL", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/fixtures/devices/da_ref_normal_01011_onedoor.json b/tests/components/smartthings/fixtures/devices/da_ref_normal_01011_onedoor.json new file mode 100644 index 00000000000..2669768b719 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_ref_normal_01011_onedoor.json @@ -0,0 +1,588 @@ +{ + "items": [ + { + "deviceId": "271d82e0-5b0c-e4b8-058e-cdf23a188610", + "name": "Samsung-Refrigerator", + "label": "Lod\u00f3wka", + "manufacturerName": "Samsung Electronics", + "presentationId": "DA-REF-NORMAL-01011", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "5274d210-9bd8-4a14-ae55-52a9ffeedfb7", + "ownerId": "d40034d0-c87b-3fa6-da98-108c42c36a6b", + "roomId": "b19fa610-62f8-4109-b9cc-47f85fcefd29", + "deviceTypeName": "Samsung OCF Refrigerator", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "ocf", + "version": 1 + }, + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "demandResponseLoadControl", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "refrigeration", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.deodorFilter", + "version": 1 + }, + { + "id": "custom.dustFilter", + "version": 1 + }, + { + "id": "custom.deviceReportStateConfiguration", + "version": 1 + }, + { + "id": "custom.energyType", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.disabledComponents", + "version": 1 + }, + { + "id": "custom.waterFilter", + "version": 1 + }, + { + "id": "samsungce.softwareUpdate", + "version": 1 + }, + { + "id": "samsungce.deviceIdentification", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.driverState", + "version": 1 + }, + { + "id": "samsungce.fridgeVacationMode", + "version": 1 + }, + { + "id": "samsungce.powerCool", + "version": 1 + }, + { + "id": "samsungce.powerFreeze", + "version": 1 + }, + { + "id": "samsungce.sabbathMode", + "version": 1 + }, + { + "id": "samsungce.selfCheck", + "version": 1 + }, + { + "id": "samsungce.viewInside", + "version": 1 + }, + { + "id": "samsungce.fridgeWelcomeLighting", + "version": 1 + }, + { + "id": "samsungce.quickControl", + "version": 1 + }, + { + "id": "samsungce.softwareVersion", + "version": 1 + }, + { + "id": "sec.diagnosticsInformation", + "version": 1 + }, + { + "id": "sec.wifiConfiguration", + "version": 1 + }, + { + "id": "sec.smartthingsHub", + "version": 1, + "ephemeral": true + } + ], + "categories": [ + { + "name": "Refrigerator", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "freezer", + "label": "freezer", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.temperatureSetting", + "version": 1 + }, + { + "id": "samsungce.freezerConvertMode", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "cooler", + "label": "cooler", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.temperatureSetting", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "cvroom", + "label": "cvroom", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "onedoor", + "label": "onedoor", + "capabilities": [ + { + "id": "contactSensor", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.temperatureSetting", + "version": 1 + }, + { + "id": "samsungce.freezerConvertMode", + "version": 1 + }, + { + "id": "samsungce.unavailableCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "icemaker", + "label": "icemaker", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "samsungce.fridgeIcemakerInfo", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "icemaker-02", + "label": "icemaker-02", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "samsungce.fridgeIcemakerInfo", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "icemaker-03", + "label": "icemaker-03", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "samsungce.fridgeIcemakerInfo", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "scale-10", + "label": "scale-10", + "capabilities": [ + { + "id": "samsungce.weightMeasurement", + "version": 1 + }, + { + "id": "samsungce.weightMeasurementCalibration", + "version": 1 + }, + { + "id": "samsungce.connectionState", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "scale-11", + "label": "scale-11", + "capabilities": [ + { + "id": "samsungce.weightMeasurement", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "pantry-01", + "label": "pantry-01", + "capabilities": [ + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "samsungce.fridgePantryInfo", + "version": 1 + }, + { + "id": "samsungce.fridgePantryMode", + "version": 1 + }, + { + "id": "samsungce.meatAging", + "version": 1 + }, + { + "id": "samsungce.foodDefrost", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "pantry-02", + "label": "pantry-02", + "capabilities": [ + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "samsungce.fridgePantryInfo", + "version": 1 + }, + { + "id": "samsungce.fridgePantryMode", + "version": 1 + }, + { + "id": "samsungce.meatAging", + "version": 1 + }, + { + "id": "samsungce.foodDefrost", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "specialzone-01", + "label": "specialzone-01", + "capabilities": [ + { + "id": "custom.fridgeMode", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2025-08-12T12:35:56.924Z", + "profile": { + "id": "840ff773-857b-324b-a54e-ba31a8155c4d" + }, + "ocf": { + "ocfDeviceType": "oic.d.refrigerator", + "name": "Samsung-Refrigerator", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "1.2.1", + "manufacturerName": "Samsung Electronics", + "modelNumber": "TP1X_REF_21K|00130445|00090026001610304100000021010000", + "platformVersion": "SYSTEM 2.0", + "platformOS": "TizenRT 4.0", + "hwVersion": "Realtek", + "firmwareVersion": "A-RFWW-TP1-24-T4-COM_20250706", + "vendorId": "DA-REF-NORMAL-01011", + "vendorResourceClientServerVersion": "MediaTek Release 250706", + "lastSignupTime": "2025-08-12T12:35:56.864318132Z", + "transferCandidate": true, + "additionalAuthCodeRequired": false, + "modelCode": "RR39C7EC5B1/EF" + }, + "type": "OCF", + "restrictionTier": 0, + "allowed": null, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 7be4d3af55b..4637de49efb 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -1169,6 +1169,55 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01011_onedoor][binary_sensor.lodowka_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.lodowka_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '271d82e0-5b0c-e4b8-058e-cdf23a188610_main_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][binary_sensor.lodowka_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Lodówka Door', + }), + 'context': , + 'entity_id': 'binary_sensor.lodowka_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_dw_000001][binary_sensor.dishwasher_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 17a9d6691cc..d732578212a 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -64,6 +64,37 @@ 'via_device_id': None, }) # --- +# name: test_devices[aq_sensor_3_ikea] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8', + ), + }), + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'aq-sensor-3-ikea', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[aux_ac] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -684,6 +715,37 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_ref_normal_01011_onedoor] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': 'Realtek', + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '271d82e0-5b0c-e4b8-058e-cdf23a188610', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'TP1X_REF_21K', + 'model_id': None, + 'name': 'Lodówka', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': 'A-RFWW-TP1-24-T4-COM_20250706', + 'via_device_id': None, + }) +# --- # name: test_devices[da_rvc_map_01011] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 169359118da..7109b46cebb 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -163,6 +163,221 @@ 'state': 'unknown', }) # --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aq_sensor_3_ikea_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8_main_relativeHumidityMeasurement_humidity_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'aq-sensor-3-ikea Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.aq_sensor_3_ikea_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '53.0', + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aq_sensor_3_ikea_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8_main_fineDustSensor_fineDustLevel_fineDustLevel', + 'unit_of_measurement': 'μg/m³', + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'aq-sensor-3-ikea PM2.5', + 'state_class': , + 'unit_of_measurement': 'μg/m³', + }), + 'context': , + 'entity_id': 'sensor.aq_sensor_3_ikea_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aq_sensor_3_ikea_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'aq-sensor-3-ikea Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.aq_sensor_3_ikea_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.0', + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_volatile_organic_compounds_parts-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aq_sensor_3_ikea_volatile_organic_compounds_parts', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Volatile organic compounds parts', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8_main_tvocMeasurement_tvocLevel_tvocLevel', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_volatile_organic_compounds_parts-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volatile_organic_compounds_parts', + 'friendly_name': 'aq-sensor-3-ikea Volatile organic compounds parts', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.aq_sensor_3_ikea_volatile_organic_compounds_parts', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) +# --- # name: test_all_entities[aux_ac][sensor.aux_a_c_on_off_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -951,7 +1166,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_veryFineDustSensor_veryFineDustLevel_veryFineDustLevel', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm1-state] @@ -960,7 +1175,7 @@ 'device_class': 'pm1', 'friendly_name': '에어모니터 플러스 PM1', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.eeomoniteo_peulreoseu_pm1', @@ -1004,7 +1219,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_dustLevel_dustLevel', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm10-state] @@ -1013,7 +1228,7 @@ 'device_class': 'pm10', 'friendly_name': '에어모니터 플러스 PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.eeomoniteo_peulreoseu_pm10', @@ -1057,7 +1272,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_fineDustLevel_fineDustLevel', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm2_5-state] @@ -1066,7 +1281,7 @@ 'device_class': 'pm25', 'friendly_name': '에어모니터 플러스 PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.eeomoniteo_peulreoseu_pm2_5', @@ -2820,7 +3035,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_dustLevel_dustLevel', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[da_ac_rac_100001][sensor.corridor_a_c_pm10-state] @@ -2829,7 +3044,7 @@ 'device_class': 'pm10', 'friendly_name': 'Corridor A/C PM10', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.corridor_a_c_pm10', @@ -2873,7 +3088,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_fineDustLevel_fineDustLevel', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }) # --- # name: test_all_entities[da_ac_rac_100001][sensor.corridor_a_c_pm2_5-state] @@ -2882,7 +3097,7 @@ 'device_class': 'pm25', 'friendly_name': 'Corridor A/C PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.corridor_a_c_pm2_5', @@ -6066,6 +6281,288 @@ 'state': '97', }) # --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lodowka_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '271d82e0-5b0c-e4b8-058e-cdf23a188610_main_powerConsumptionReport_powerConsumption_energy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Lodówka Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lodowka_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.861', + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_energy_difference-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lodowka_energy_difference', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy difference', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_difference', + 'unique_id': '271d82e0-5b0c-e4b8-058e-cdf23a188610_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_energy_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Lodówka Energy difference', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lodowka_energy_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_energy_saved-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lodowka_energy_saved', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy saved', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_saved', + 'unique_id': '271d82e0-5b0c-e4b8-058e-cdf23a188610_main_powerConsumptionReport_powerConsumption_energySaved_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_energy_saved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Lodówka Energy saved', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lodowka_energy_saved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lodowka_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '271d82e0-5b0c-e4b8-058e-cdf23a188610_main_powerConsumptionReport_powerConsumption_power_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Lodówka Power', + 'power_consumption_end': '2025-08-14T07:21:35Z', + 'power_consumption_start': '2025-08-14T07:04:50Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lodowka_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_power_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lodowka_power_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power_energy', + 'unique_id': '271d82e0-5b0c-e4b8-058e-cdf23a188610_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][sensor.lodowka_power_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Lodówka Power energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lodowka_power_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.00027936416665713', + }) +# --- # name: test_all_entities[da_rvc_map_01011][sensor.robot_vacuum_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index 1aaeb35205f..6512e88998b 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -623,6 +623,54 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01011_onedoor][switch.lodowka_power_cool-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.lodowka_power_cool', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power cool', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power_cool', + 'unique_id': '271d82e0-5b0c-e4b8-058e-cdf23a188610_main_samsungce.powerCool_activated_activated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011_onedoor][switch.lodowka_power_cool-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Lodówka Power cool', + }), + 'context': , + 'entity_id': 'switch.lodowka_power_cool', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_rvc_map_01011][switch.robot_vacuum-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_update.ambr b/tests/components/smartthings/snapshots/test_update.ambr index 3191411a429..eb6b99b3363 100644 --- a/tests/components/smartthings/snapshots/test_update.ambr +++ b/tests/components/smartthings/snapshots/test_update.ambr @@ -1,4 +1,65 @@ # serializer version: 1 +# name: test_all_entities[aq_sensor_3_ikea][update.aq_sensor_3_ikea_firmware-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.aq_sensor_3_ikea_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Firmware', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[aq_sensor_3_ikea][update.aq_sensor_3_ikea_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/smartthings/icon.png', + 'friendly_name': 'aq-sensor-3-ikea Firmware', + 'in_progress': False, + 'installed_version': '00010010', + 'latest_version': '00010010', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.aq_sensor_3_ikea_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[bosch_radiator_thermostat_ii][update.radiator_thermostat_ii_m_wohnzimmer_firmware-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/snapcast/conftest.py b/tests/components/snapcast/conftest.py index c2c4ffa7997..282429b110a 100644 --- a/tests/components/snapcast/conftest.py +++ b/tests/components/snapcast/conftest.py @@ -36,8 +36,10 @@ def mock_server(mock_create_server: AsyncMock) -> Generator[AsyncMock]: @pytest.fixture def mock_create_server( - mock_group: AsyncMock, - mock_client: AsyncMock, + mock_group_1: AsyncMock, + mock_group_2: AsyncMock, + mock_client_1: AsyncMock, + mock_client_2: AsyncMock, mock_stream_1: AsyncMock, mock_stream_2: AsyncMock, ) -> Generator[AsyncMock]: @@ -46,16 +48,26 @@ def mock_create_server( "homeassistant.components.snapcast.coordinator.Snapserver", autospec=True ) as mock_snapserver: mock_server = mock_snapserver.return_value - mock_server.groups = [mock_group] - mock_server.clients = [mock_client] + mock_server.groups = [mock_group_1, mock_group_2] + mock_server.clients = [mock_client_1, mock_client_2] mock_server.streams = [mock_stream_1, mock_stream_2] - mock_server.group.return_value = mock_group - mock_server.client.return_value = mock_client def get_stream(identifier: str) -> AsyncMock: return {s.identifier: s for s in mock_server.streams}[identifier] + def get_group(identifier: str) -> AsyncMock: + return {s.identifier: s for s in mock_server.groups}[identifier] + + def get_client(identifier: str) -> AsyncMock: + return {s.identifier: s for s in mock_server.clients}[identifier] + mock_server.stream = get_stream + mock_server.group = get_group + mock_server.client = get_client + + mock_client_1.groups_available = lambda: mock_server.groups + mock_client_2.groups_available = lambda: mock_server.groups + yield mock_server @@ -74,34 +86,66 @@ async def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_group(stream: str, streams: dict[str, AsyncMock]) -> AsyncMock: +def mock_group_1(mock_stream_1: AsyncMock, streams: dict[str, AsyncMock]) -> AsyncMock: """Create a mock Snapgroup.""" group = AsyncMock(spec=Snapgroup) group.identifier = "4dcc4e3b-c699-a04b-7f0c-8260d23c43e1" - group.name = "test_group" - group.friendly_name = "test_group" - group.stream = stream + group.name = "test_group_1" + group.friendly_name = "Test Group 1" + group.stream = mock_stream_1.identifier group.muted = False - group.stream_status = streams[stream].status + group.stream_status = mock_stream_1.status group.volume = 48 group.streams_by_name.return_value = {s.friendly_name: s for s in streams.values()} return group @pytest.fixture -def mock_client(mock_group: AsyncMock) -> AsyncMock: +def mock_group_2(mock_stream_2: AsyncMock, streams: dict[str, AsyncMock]) -> AsyncMock: + """Create a mock Snapgroup.""" + group = AsyncMock(spec=Snapgroup) + group.identifier = "4dcc4e3b-c699-a04b-7f0c-8260d23c43e2" + group.name = "test_group_2" + group.friendly_name = "Test Group 2" + group.stream = mock_stream_2.identifier + group.muted = False + group.stream_status = mock_stream_2.status + group.volume = 65 + group.streams_by_name.return_value = {s.friendly_name: s for s in streams.values()} + return group + + +@pytest.fixture +def mock_client_1(mock_group_1: AsyncMock) -> AsyncMock: """Create a mock Snapclient.""" client = AsyncMock(spec=Snapclient) - client.identifier = "00:21:6a:7d:74:fc#2" - client.friendly_name = "test_client" + client.identifier = "00:21:6a:7d:74:fc#1" + client.friendly_name = "test_client_1" client.version = "0.10.0" client.connected = True - client.name = "Snapclient" + client.name = "Snapclient 1" client.latency = 6 client.muted = False client.volume = 48 - client.group = mock_group - mock_group.clients = [client.identifier] + client.group = mock_group_1 + mock_group_1.clients = [client.identifier] + return client + + +@pytest.fixture +def mock_client_2(mock_group_2: AsyncMock) -> AsyncMock: + """Create a mock Snapclient.""" + client = AsyncMock(spec=Snapclient) + client.identifier = "00:21:6a:7d:74:fc#2" + client.friendly_name = "test_client_2" + client.version = "0.10.0" + client.connected = True + client.name = "Snapclient 2" + client.latency = 6 + client.muted = False + client.volume = 100 + client.group = mock_group_2 + mock_group_2.clients = [client.identifier] return client @@ -149,17 +193,6 @@ def mock_stream_2() -> AsyncMock: return stream -@pytest.fixture( - params=[ - "test_stream_1", - "test_stream_2", - ] -) -def stream(request: pytest.FixtureRequest) -> Generator[str]: - """Return every device.""" - return request.param - - @pytest.fixture def streams(mock_stream_1: AsyncMock, mock_stream_2: AsyncMock) -> dict[str, AsyncMock]: """Return a dictionary of mock streams.""" diff --git a/tests/components/snapcast/snapshots/test_media_player.ambr b/tests/components/snapcast/snapshots/test_media_player.ambr index c497cdd861b..3e408a0f14e 100644 --- a/tests/components/snapcast/snapshots/test_media_player.ambr +++ b/tests/components/snapcast/snapshots/test_media_player.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_state[test_stream_1][media_player.test_client_snapcast_client-entry] +# name: test_state[media_player.test_client_1_snapcast_client-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -17,7 +17,7 @@ 'disabled_by': None, 'domain': 'media_player', 'entity_category': None, - 'entity_id': 'media_player.test_client_snapcast_client', + 'entity_id': 'media_player.test_client_1_snapcast_client', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -29,22 +29,25 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test_client Snapcast Client', + 'original_name': 'test_client_1 Snapcast Client', 'platform': 'snapcast', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, - 'unique_id': 'snapcast_client_127.0.0.1:1705_00:21:6a:7d:74:fc#2', + 'unique_id': 'snapcast_client_127.0.0.1:1705_00:21:6a:7d:74:fc#1', 'unit_of_measurement': None, }) # --- -# name: test_state[test_stream_1][media_player.test_client_snapcast_client-state] +# name: test_state[media_player.test_client_1_snapcast_client-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'speaker', - 'entity_picture': '/api/media_player_proxy/media_player.test_client_snapcast_client?token=mock_token&cache=6e2dee674d9d1dc7', - 'friendly_name': 'test_client Snapcast Client', + 'entity_picture': '/api/media_player_proxy/media_player.test_client_1_snapcast_client?token=mock_token&cache=6e2dee674d9d1dc7', + 'friendly_name': 'test_client_1 Snapcast Client', + 'group_members': list([ + 'media_player.test_client_1_snapcast_client', + ]), 'is_volume_muted': False, 'latency': 6, 'media_album_artist': 'Test Album Artist 1, Test Album Artist 2', @@ -60,18 +63,18 @@ 'Test Stream 1', 'Test Stream 2', ]), - 'supported_features': , + 'supported_features': , 'volume_level': 0.48, }), 'context': , - 'entity_id': 'media_player.test_client_snapcast_client', + 'entity_id': 'media_player.test_client_1_snapcast_client', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'playing', }) # --- -# name: test_state[test_stream_1][media_player.test_group_snapcast_group-entry] +# name: test_state[media_player.test_client_2_snapcast_client-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -89,7 +92,7 @@ 'disabled_by': None, 'domain': 'media_player', 'entity_category': None, - 'entity_id': 'media_player.test_group_snapcast_group', + 'entity_id': 'media_player.test_client_2_snapcast_client', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -101,7 +104,74 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test_group Snapcast Group', + 'original_name': 'test_client_2 Snapcast Client', + 'platform': 'snapcast', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'snapcast_client_127.0.0.1:1705_00:21:6a:7d:74:fc#2', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[media_player.test_client_2_snapcast_client-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speaker', + 'friendly_name': 'test_client_2 Snapcast Client', + 'group_members': list([ + 'media_player.test_client_2_snapcast_client', + ]), + 'is_volume_muted': False, + 'latency': 6, + 'media_content_type': , + 'source': 'test_stream_2', + 'source_list': list([ + 'Test Stream 1', + 'Test Stream 2', + ]), + 'supported_features': , + 'volume_level': 1.0, + }), + 'context': , + 'entity_id': 'media_player.test_client_2_snapcast_client', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_state[media_player.test_group_1_snapcast_group-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'source_list': list([ + 'Test Stream 1', + 'Test Stream 2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'media_player', + 'entity_category': None, + 'entity_id': 'media_player.test_group_1_snapcast_group', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Test Group 1 Snapcast Group', 'platform': 'snapcast', 'previous_unique_id': None, 'suggested_object_id': None, @@ -111,12 +181,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_state[test_stream_1][media_player.test_group_snapcast_group-state] +# name: test_state[media_player.test_group_1_snapcast_group-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'speaker', - 'entity_picture': '/api/media_player_proxy/media_player.test_group_snapcast_group?token=mock_token&cache=6e2dee674d9d1dc7', - 'friendly_name': 'test_group Snapcast Group', + 'entity_picture': '/api/media_player_proxy/media_player.test_group_1_snapcast_group?token=mock_token&cache=6e2dee674d9d1dc7', + 'friendly_name': 'Test Group 1 Snapcast Group', 'is_volume_muted': False, 'media_album_artist': 'Test Album Artist 1, Test Album Artist 2', 'media_album_name': 'Test Album', @@ -135,14 +205,14 @@ 'volume_level': 0.48, }), 'context': , - 'entity_id': 'media_player.test_group_snapcast_group', + 'entity_id': 'media_player.test_group_1_snapcast_group', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'playing', }) # --- -# name: test_state[test_stream_2][media_player.test_client_snapcast_client-entry] +# name: test_state[media_player.test_group_2_snapcast_group-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -160,7 +230,7 @@ 'disabled_by': None, 'domain': 'media_player', 'entity_category': None, - 'entity_id': 'media_player.test_client_snapcast_client', + 'entity_id': 'media_player.test_group_2_snapcast_group', 'has_entity_name': False, 'hidden_by': None, 'icon': None, @@ -172,85 +242,21 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test_client Snapcast Client', + 'original_name': 'Test Group 2 Snapcast Group', 'platform': 'snapcast', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'snapcast_client_127.0.0.1:1705_00:21:6a:7d:74:fc#2', + 'unique_id': 'snapcast_group_127.0.0.1:1705_4dcc4e3b-c699-a04b-7f0c-8260d23c43e2', 'unit_of_measurement': None, }) # --- -# name: test_state[test_stream_2][media_player.test_client_snapcast_client-state] +# name: test_state[media_player.test_group_2_snapcast_group-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'speaker', - 'friendly_name': 'test_client Snapcast Client', - 'is_volume_muted': False, - 'latency': 6, - 'media_content_type': , - 'source': 'test_stream_2', - 'source_list': list([ - 'Test Stream 1', - 'Test Stream 2', - ]), - 'supported_features': , - 'volume_level': 0.48, - }), - 'context': , - 'entity_id': 'media_player.test_client_snapcast_client', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'idle', - }) -# --- -# name: test_state[test_stream_2][media_player.test_group_snapcast_group-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'source_list': list([ - 'Test Stream 1', - 'Test Stream 2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'media_player', - 'entity_category': None, - 'entity_id': 'media_player.test_group_snapcast_group', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'test_group Snapcast Group', - 'platform': 'snapcast', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'snapcast_group_127.0.0.1:1705_4dcc4e3b-c699-a04b-7f0c-8260d23c43e1', - 'unit_of_measurement': None, - }) -# --- -# name: test_state[test_stream_2][media_player.test_group_snapcast_group-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'speaker', - 'friendly_name': 'test_group Snapcast Group', + 'friendly_name': 'Test Group 2 Snapcast Group', 'is_volume_muted': False, 'media_content_type': , 'source': 'test_stream_2', @@ -259,10 +265,10 @@ 'Test Stream 2', ]), 'supported_features': , - 'volume_level': 0.48, + 'volume_level': 0.65, }), 'context': , - 'entity_id': 'media_player.test_group_snapcast_group', + 'entity_id': 'media_player.test_group_2_snapcast_group', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/snapcast/test_media_player.py b/tests/components/snapcast/test_media_player.py index 57a8a865ddf..35605cb74ab 100644 --- a/tests/components/snapcast/test_media_player.py +++ b/tests/components/snapcast/test_media_player.py @@ -2,11 +2,23 @@ from unittest.mock import AsyncMock, patch +import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.media_player import ( + ATTR_GROUP_MEMBERS, + ATTR_MEDIA_VOLUME_LEVEL, + DOMAIN as MEDIA_PLAYER_DOMAIN, + SERVICE_JOIN, + SERVICE_UNJOIN, + SERVICE_VOLUME_SET, +) +from homeassistant.components.snapcast.const import ATTR_MASTER, DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er, issue_registry as ir from . import setup_integration @@ -28,3 +40,190 @@ async def test_state( assert mock_config_entry.state is ConfigEntryState.LOADED await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("members"), + [ + ["media_player.test_client_2_snapcast_client"], + [ + "media_player.test_client_1_snapcast_client", + "media_player.test_client_2_snapcast_client", + ], + ], +) +async def test_join( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_create_server: AsyncMock, + mock_group_1: AsyncMock, + mock_client_2: AsyncMock, + members: list[str], +) -> None: + """Test grouping of media players through the join service.""" + + # Setup and verify the integration is loaded + with patch("secrets.token_hex", return_value="mock_token"): + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_JOIN, + { + ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client", + ATTR_GROUP_MEMBERS: members, + }, + blocking=True, + ) + mock_group_1.add_client.assert_awaited_once_with(mock_client_2.identifier) + + +async def test_unjoin( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_create_server: AsyncMock, + mock_client_1: AsyncMock, + mock_group_1: AsyncMock, +) -> None: + """Test the unjoin service removes the client from the group.""" + + # Setup and verify the integration is loaded + with patch("secrets.token_hex", return_value="mock_token"): + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_UNJOIN, + { + ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client", + }, + blocking=True, + ) + + mock_group_1.remove_client.assert_awaited_once_with(mock_client_1.identifier) + + +async def test_join_exception( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_create_server: AsyncMock, + mock_group_1: AsyncMock, +) -> None: + """Test join service throws an exception when trying to add a non-Snapcast client.""" + + # Create a dummy media player entity + entity_registry.async_get_or_create( + MEDIA_PLAYER_DOMAIN, + "dummy", + "media_player_1", + ) + await hass.async_block_till_done() + + # Setup and verify the integration is loaded + with patch("secrets.token_hex", return_value="mock_token"): + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.LOADED + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_JOIN, + { + ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client", + ATTR_GROUP_MEMBERS: ["media_player.dummy_media_player_1"], + }, + blocking=True, + ) + + # Ensure that the group did not attempt to add a non-Snapcast client + mock_group_1.add_client.assert_not_awaited() + + +async def test_legacy_join_issue( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + mock_config_entry: MockConfigEntry, + mock_create_server: AsyncMock, +) -> None: + """Test the legacy grouping services create issues when used.""" + + # Setup and verify the integration is loaded + with patch("secrets.token_hex", return_value="mock_token"): + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Call the legacy join service + await hass.services.async_call( + DOMAIN, + SERVICE_JOIN, + { + ATTR_ENTITY_ID: "media_player.test_client_2_snapcast_client", + ATTR_MASTER: "media_player.test_client_1_snapcast_client", + }, + blocking=True, + ) + + # Verify the issue is created + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="deprecated_grouping_actions", + ) + assert issue is not None + + # Clear existing issue + issue_registry.async_delete( + domain=DOMAIN, + issue_id="deprecated_grouping_actions", + ) + + # Call legacy unjoin service + await hass.services.async_call( + DOMAIN, + SERVICE_UNJOIN, + { + ATTR_ENTITY_ID: "media_player.test_client_2_snapcast_client", + }, + blocking=True, + ) + + # Verify the issue is created again + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="deprecated_grouping_actions", + ) + assert issue is not None + + +async def test_deprecated_group_entity_issue( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + mock_config_entry: MockConfigEntry, + mock_create_server: AsyncMock, +) -> None: + """Test the legacy group entities create issues when used.""" + + # Setup and verify the integration is loaded + with patch("secrets.token_hex", return_value="mock_token"): + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Call a servuce that uses a group entity + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_VOLUME_SET, + service_data={ + ATTR_ENTITY_ID: "media_player.test_group_1_snapcast_group", + ATTR_MEDIA_VOLUME_LEVEL: 0.5, + }, + blocking=True, + ) + + # Verify the issue is created + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id="deprecated_group_entities", + ) + assert issue is not None diff --git a/tests/components/snoo/__init__.py b/tests/components/snoo/__init__.py index b4692e6f08b..417eb438143 100644 --- a/tests/components/snoo/__init__.py +++ b/tests/components/snoo/__init__.py @@ -48,7 +48,7 @@ def find_update_callback( mock: AsyncMock, serial_number: str ) -> Callable[[SnooData], Awaitable[None]]: """Find the update callback for a specific identifier.""" - for call in mock.subscribe.call_args_list: + for call in mock.start_subscribe.call_args_list: if call[0][0].serialNumber == serial_number: return call[0][1] pytest.fail(f"Callback for identifier {serial_number} not found") diff --git a/tests/components/snoo/const.py b/tests/components/snoo/const.py index 2657048afb8..cd52679caf9 100644 --- a/tests/components/snoo/const.py +++ b/tests/components/snoo/const.py @@ -31,7 +31,12 @@ MOCK_SNOO_DEVICES = [ "name": "Test Snoo", "presence": {}, "presenceIoT": {}, - "awsIoT": {}, + "awsIoT": { + "awsRegion": "us-east-1", + "clientEndpoint": "z00023244d7fia4appr4b-ats.iot.us-east-1.amazonaws.com", + "clientReady": True, + "thingName": "676cbbe74529f85038b2e623_5831231335004715141_prod", + }, "lastSSID": {}, "provisionedAt": "random_time", } diff --git a/tests/components/solarlog/fixtures/solarlog_data.json b/tests/components/solarlog/fixtures/solarlog_data.json index 339ab4a4dfc..be29194a783 100644 --- a/tests/components/solarlog/fixtures/solarlog_data.json +++ b/tests/components/solarlog/fixtures/solarlog_data.json @@ -21,5 +21,10 @@ "usage": 54.8, "power_available": 45.13, "capacity": 85.5, - "last_updated": "2024-08-01T15:20:45Z" + "last_updated": "2024-08-01T15:20:45Z", + "battery_data": { + "charge_power": 1074, + "discharge_power": 0, + "level": 79 + } } diff --git a/tests/components/solarlog/snapshots/test_diagnostics.ambr b/tests/components/solarlog/snapshots/test_diagnostics.ambr index 6aef72ebbd5..212742b82f0 100644 --- a/tests/components/solarlog/snapshots/test_diagnostics.ambr +++ b/tests/components/solarlog/snapshots/test_diagnostics.ambr @@ -26,6 +26,12 @@ }), 'solarlog_data': dict({ 'alternator_loss': 2.0, + 'battery_data': dict({ + 'charge_power': 1074.0, + 'discharge_power': 0.0, + 'level': 79.0, + 'voltage': 0, + }), 'capacity': 85.5, 'consumption_ac': 54.87, 'consumption_day': 5.31, diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr index 8f0ee17df44..0ddccf6a193 100644 --- a/tests/components/solarlog/snapshots/test_sensor.ambr +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -341,6 +341,115 @@ 'state': '85.5', }) # --- +# name: test_all_entities[sensor.solarlog_charge_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_charge_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge level', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charge_level', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_charge_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solarlog_charge_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'SolarLog Charge level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solarlog_charge_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '79.0', + }) +# --- +# name: test_all_entities[sensor.solarlog_charging_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_charging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging power', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_power', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_charging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_charging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SolarLog Charging power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_charging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1074.0', + }) +# --- # name: test_all_entities[sensor.solarlog_consumption_ac-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -689,6 +798,62 @@ 'state': '0.00734', }) # --- +# name: test_all_entities[sensor.solarlog_discharging_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solarlog_discharging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Discharging power', + 'platform': 'solarlog', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'discharging_power', + 'unique_id': 'ce5f5431554d101905d31797e1232da8_discharging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solarlog_discharging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SolarLog Discharging power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarlog_discharging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_entities[sensor.solarlog_efficiency-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index d3de2a889d5..6831e4139c2 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -167,16 +167,26 @@ async def async_autosetup_sonos(async_setup_sonos): await async_setup_sonos() +def reset_sonos_alarms(alarm_event: SonosMockEvent) -> None: + """Reset the Sonos alarms to a known state.""" + sonos_alarms = Alarms() + sonos_alarms.alarms = {} + sonos_alarms._last_zone_used = None + sonos_alarms._last_alarm_list_version = None + sonos_alarms.last_uid = None + sonos_alarms.last_id = 0 + alarm_event.variables["alarm_list_version"] = "RINCON_test:0" + + @pytest.fixture def async_setup_sonos( - hass: HomeAssistant, config_entry: MockConfigEntry, fire_zgs_event + hass: HomeAssistant, config_entry: MockConfigEntry, fire_zgs_event, alarm_event ) -> Callable[[], Coroutine[Any, Any, None]]: """Return a coroutine to set up a Sonos integration instance on demand.""" async def _wrapper(): config_entry.add_to_hass(hass) - sonos_alarms = Alarms() - sonos_alarms.last_alarm_list_version = "RINCON_test:0" + reset_sonos_alarms(alarm_event) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) await fire_zgs_event() @@ -214,12 +224,26 @@ class MockSoCo(MagicMock): surround_level = 3 music_surround_level = 4 soundbar_audio_input_format = "Dolby 5.1" + factory: SoCoMockFactory | None = None @property def visible_zones(self): """Return visible zones and allow property to be overridden by device classes.""" return {self} + @property + def all_zones(self) -> set[MockSoCo]: + """Return all mock zones if a factory is set and enabled, else just self.""" + return ( + self.factory.mock_all_zones + if self.factory and self.factory.mock_all_zones + else {self} + ) + + def set_factory(self, factory: SoCoMockFactory) -> None: + """Set the factory for this mock.""" + self.factory = factory + class SoCoMockFactory: """Factory for creating SoCo Mocks.""" @@ -243,12 +267,19 @@ class SoCoMockFactory: self.alarm_clock = alarm_clock self.sonos_playlists = sonos_playlists self.sonos_queue = sonos_queue + self.mock_zones: bool = False + + @property + def mock_all_zones(self) -> set[MockSoCo] | None: + """Return a set of all mock zones, or None if not enabled.""" + return set(self.mock_list.values()) if self.mock_zones else None def cache_mock( self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A" ) -> MockSoCo: """Put a user created mock into the cache.""" mock_soco.mock_add_spec(SoCo) + mock_soco.set_factory(self) mock_soco.ip_address = ip_address if ip_address != "192.168.42.2": mock_soco.uid += f"_{ip_address}" @@ -257,9 +288,15 @@ class SoCoMockFactory: mock_soco.music_source_from_uri = SoCo.music_source_from_uri mock_soco.get_sonos_playlists.return_value = self.sonos_playlists mock_soco.get_queue.return_value = self.sonos_queue + mock_soco._player_name = name my_speaker_info = self.speaker_info.copy() my_speaker_info["zone_name"] = name my_speaker_info["uid"] = mock_soco.uid + # Generate a different MAC for the non-default speakers. + # otherwise new devices will not be created. + if ip_address != "192.168.42.2": + last_octet = ip_address.split(".")[-1] + my_speaker_info["mac_address"] = f"00-00-00-00-00-{last_octet.zfill(2)}" mock_soco.get_speaker_info = Mock(return_value=my_speaker_info) mock_soco.add_to_queue = Mock(return_value=10) mock_soco.add_uri_to_queue = Mock(return_value=10) @@ -278,7 +315,6 @@ class SoCoMockFactory: mock_soco.alarmClock = self.alarm_clock mock_soco.get_battery_info.return_value = self.battery_info - mock_soco.all_zones = {mock_soco} mock_soco.group.coordinator = mock_soco mock_soco.household_id = "test_household_id" self.mock_list[ip_address] = mock_soco @@ -824,11 +860,15 @@ def zgs_event_fixture( @pytest.fixture(name="sonos_setup_two_speakers") async def sonos_setup_two_speakers( - hass: HomeAssistant, soco_factory: SoCoMockFactory + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + alarm_event: SonosMockEvent, ) -> list[MockSoCo]: """Set up home assistant with two Sonos Speakers.""" soco_lr = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") soco_br = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") + reset_sonos_alarms(alarm_event) + await async_setup_component( hass, DOMAIN, @@ -882,3 +922,23 @@ def ungroup_speakers(coordinator: MockSoCo, group_member: MockSoCo) -> None: ) coordinator.zoneGroupTopology.subscribe.return_value._callback(event) group_member.zoneGroupTopology.subscribe.return_value._callback(event) + + +def create_rendering_control_event( + soco: MockSoCo, +) -> SonosMockEvent: + """Create a Sonos Event for speaker rendering control.""" + variables = { + "dialog_level": 1, + "speech_enhance_enable": 1, + "surround_level": 6, + "music_surround_level": 4, + "audio_delay": 0, + "audio_delay_left_rear": 0, + "audio_delay_right_rear": 0, + "night_mode": 0, + "surround_enabled": 1, + "surround_mode": 1, + "height_channel_level": 1, + } + return SonosMockEvent(soco, soco.renderingControl, variables) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 84ad624cdc8..41b18750fd4 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -33,14 +33,20 @@ from homeassistant.components.sonos.const import ( SOURCE_TV, ) from homeassistant.components.sonos.media_player import ( + ATTR_ALARM_ID, + ATTR_ENABLED, + ATTR_INCLUDE_LINKED_ZONES, + ATTR_VOLUME, LONG_SERVICE_TIMEOUT, SERVICE_GET_QUEUE, SERVICE_RESTORE, SERVICE_SNAPSHOT, + SERVICE_UPDATE_ALARM, VOLUME_INCREMENT, ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_TIME, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -1265,3 +1271,67 @@ async def test_media_source_list( """Test the mapping between the speaker model name and source_list.""" state = hass.states.get("media_player.zone_a") assert state.attributes.get(ATTR_INPUT_SOURCE_LIST) == source_list + + +async def test_service_update_alarm( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, +) -> None: + """Test updating an alarm.""" + + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_ALARM, + { + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_ALARM_ID: 14, + ATTR_TIME: "07:15:00", + ATTR_VOLUME: 0.25, + ATTR_INCLUDE_LINKED_ZONES: True, + ATTR_ENABLED: True, + }, + blocking=True, + ) + + assert soco.alarmClock.UpdateAlarm.call_count == 1 + assert soco.alarmClock.UpdateAlarm.call_args.args[0] == [ + ("ID", "14"), + ("StartLocalTime", "07:15:00"), + ("Duration", "02:00:00"), + ("Recurrence", "DAILY"), + ("Enabled", "1"), + ("RoomUUID", "RINCON_test"), + ("ProgramURI", "x-rincon-buzzer:0"), + ("ProgramMetaData", ""), + ("PlayMode", "SHUFFLE_NOREPEAT"), + ("Volume", 25), + ("IncludeLinkedZones", "1"), + ] + + +async def test_service_update_alarm_dne( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, +) -> None: + """Test updating an alarm that does not exist.""" + + with pytest.raises( + ServiceValidationError, + match="Alarm 99 does not exist and cannot be updated", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_ALARM, + { + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_ALARM_ID: 99, + ATTR_TIME: "07:15:00", + ATTR_VOLUME: 0.25, + ATTR_INCLUDE_LINKED_ZONES: True, + ATTR_ENABLED: True, + }, + blocking=True, + ) + assert soco.alarmClock.UpdateAlarm.call_count == 0 diff --git a/tests/components/sonos/test_select.py b/tests/components/sonos/test_select.py new file mode 100644 index 00000000000..ada48de21f3 --- /dev/null +++ b/tests/components/sonos/test_select.py @@ -0,0 +1,189 @@ +"""Tests for the Sonos select platform.""" + +import logging +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.sonos.const import ( + ATTR_DIALOG_LEVEL, + MODEL_SONOS_ARC_ULTRA, + SCAN_INTERVAL, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import create_rendering_control_event + +from tests.common import async_fire_time_changed + +SELECT_DIALOG_LEVEL_ENTITY = "select.zone_a_speech_enhancement" + + +@pytest.fixture(name="platform_select", autouse=True) +async def platform_binary_sensor_fixture(): + """Patch Sonos to only load select platform.""" + with patch("homeassistant.components.sonos.PLATFORMS", [Platform.SELECT]): + yield + + +@pytest.mark.parametrize( + ("level", "result"), + [ + (0, "off"), + (1, "low"), + (2, "medium"), + (3, "high"), + (4, "max"), + ], +) +async def test_select_dialog_level( + hass: HomeAssistant, + async_setup_sonos, + soco, + entity_registry: er.EntityRegistry, + speaker_info: dict[str, str], + level: int, + result: str, +) -> None: + """Test dialog level select entity.""" + + speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() + soco.get_speaker_info.return_value = speaker_info + soco.dialog_level = level + + await async_setup_sonos() + + dialog_level_select = entity_registry.entities[SELECT_DIALOG_LEVEL_ENTITY] + dialog_level_state = hass.states.get(dialog_level_select.entity_id) + assert dialog_level_state.state == result + + +async def test_select_dialog_invalid_level( + hass: HomeAssistant, + async_setup_sonos, + soco, + entity_registry: er.EntityRegistry, + speaker_info: dict[str, str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test receiving an invalid level from the speaker.""" + + speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() + soco.get_speaker_info.return_value = speaker_info + soco.dialog_level = 10 + + with caplog.at_level(logging.WARNING): + await async_setup_sonos() + assert "Invalid option 10 for dialog_level" in caplog.text + + dialog_level_select = entity_registry.entities[SELECT_DIALOG_LEVEL_ENTITY] + dialog_level_state = hass.states.get(dialog_level_select.entity_id) + assert dialog_level_state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + ("result", "option"), + [ + (0, "off"), + (1, "low"), + (2, "medium"), + (3, "high"), + (4, "max"), + ], +) +async def test_select_dialog_level_set( + hass: HomeAssistant, + async_setup_sonos, + soco, + speaker_info: dict[str, str], + result: int, + option: str, +) -> None: + """Test setting dialog level select entity.""" + + speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() + soco.get_speaker_info.return_value = speaker_info + soco.dialog_level = 0 + + await async_setup_sonos() + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: SELECT_DIALOG_LEVEL_ENTITY, ATTR_OPTION: option}, + blocking=True, + ) + + assert soco.dialog_level == result + + +async def test_select_dialog_level_only_arc_ultra( + hass: HomeAssistant, + async_setup_sonos, + entity_registry: er.EntityRegistry, + speaker_info: dict[str, str], +) -> None: + """Test the dialog level select is only created for Sonos Arc Ultra.""" + + speaker_info["model_name"] = "Sonos S1" + await async_setup_sonos() + + assert SELECT_DIALOG_LEVEL_ENTITY not in entity_registry.entities + + +async def test_select_dialog_level_event( + hass: HomeAssistant, + async_setup_sonos, + soco, + entity_registry: er.EntityRegistry, + speaker_info: dict[str, str], +) -> None: + """Test dialog level select entity updated by event.""" + + speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() + soco.get_speaker_info.return_value = speaker_info + soco.dialog_level = 0 + + await async_setup_sonos() + + event = create_rendering_control_event(soco) + event.variables[ATTR_DIALOG_LEVEL] = 3 + soco.renderingControl.subscribe.return_value._callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + + dialog_level_select = entity_registry.entities[SELECT_DIALOG_LEVEL_ENTITY] + dialog_level_state = hass.states.get(dialog_level_select.entity_id) + assert dialog_level_state.state == "high" + + +async def test_select_dialog_level_poll( + hass: HomeAssistant, + async_setup_sonos, + soco, + entity_registry: er.EntityRegistry, + speaker_info: dict[str, str], + freezer: FrozenDateTimeFactory, +) -> None: + """Test entity updated by poll when subscription fails.""" + + speaker_info["model_name"] = MODEL_SONOS_ARC_ULTRA.lower() + soco.get_speaker_info.return_value = speaker_info + soco.dialog_level = 0 + + await async_setup_sonos() + + soco.dialog_level = 4 + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + dialog_level_select = entity_registry.entities[SELECT_DIALOG_LEVEL_ENTITY] + dialog_level_state = hass.states.get(dialog_level_select.entity_id) + assert dialog_level_state.state == "max" diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 04457ee95c7..f2dd3478a90 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -6,13 +6,19 @@ from unittest.mock import patch import pytest -from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER +from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos.const import ( + DATA_SONOS_DISCOVERY_MANAGER, + MODEL_SONOS_ARC_ULTRA, +) from homeassistant.components.sonos.switch import ( ATTR_DURATION, ATTR_ID, ATTR_INCLUDE_LINKED_ZONES, ATTR_PLAY_MODE, ATTR_RECURRENCE, + ATTR_SPEECH_ENHANCEMENT, + ATTR_SPEECH_ENHANCEMENT_ENABLED, ATTR_VOLUME, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -26,10 +32,17 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from .conftest import MockSoCo, SonosMockEvent +from .conftest import ( + MockSoCo, + SoCoMockFactory, + SonosMockEvent, + SonosMockService, + create_rendering_control_event, +) from tests.common import async_fire_time_changed @@ -142,6 +155,49 @@ async def test_switch_attributes( assert touch_controls_state.state == STATE_ON +@pytest.mark.parametrize( + ("model", "attribute"), + [ + ("Sonos One SL", ATTR_SPEECH_ENHANCEMENT), + (MODEL_SONOS_ARC_ULTRA.lower(), ATTR_SPEECH_ENHANCEMENT_ENABLED), + ], +) +async def test_switch_speech_enhancement( + hass: HomeAssistant, + async_setup_sonos, + soco: MockSoCo, + speaker_info: dict[str, str], + entity_registry: er.EntityRegistry, + model: str, + attribute: str, +) -> None: + """Tests the speech enhancement switch and attribute substitution for different models.""" + entity_id = "switch.zone_a_speech_enhancement" + speaker_info["model_name"] = model + soco.get_speaker_info.return_value = speaker_info + setattr(soco, attribute, True) + await async_setup_sonos() + switch = entity_registry.entities[entity_id] + state = hass.states.get(switch.entity_id) + assert state.state == STATE_ON + + event = create_rendering_control_event(soco) + event.variables[attribute] = False + soco.renderingControl.subscribe.return_value._callback(event) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(switch.entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert getattr(soco, attribute) is True + + @pytest.mark.parametrize( ("service", "expected_result"), [ @@ -211,3 +267,73 @@ async def test_alarm_create_delete( assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" not in entity_registry.entities + + +async def test_alarm_change_device( + hass: HomeAssistant, + alarm_clock: SonosMockService, + alarm_clock_extended: SonosMockService, + alarm_event: SonosMockEvent, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + soco_factory: SoCoMockFactory, +) -> None: + """Test Sonos Alarm being moved to a different speaker. + + This test simulates a scenario where an alarm is created on one speaker + and then moved to another speaker. It checks that the entity is correctly + created on the new speaker and removed from the old one. + """ + + # Create the alarm on the soco_lr speaker + soco_factory.mock_zones = True + soco_lr = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") + alarm_dict = copy(alarm_clock.ListAlarms.return_value) + alarm_dict["CurrentAlarmList"] = alarm_dict["CurrentAlarmList"].replace( + "RINCON_test", f"{soco_lr.uid}" + ) + alarm_dict["CurrentAlarmListVersion"] = "RINCON_test:900" + soco_lr.alarmClock.ListAlarms.return_value = alarm_dict + soco_br = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "media_player": { + "interface_addr": "127.0.0.1", + "hosts": ["10.10.10.1", "10.10.10.2"], + } + } + }, + ) + await hass.async_block_till_done() + + entity_id = "switch.sonos_alarm_14" + + # Verify the alarm is created on the soco_lr speaker + assert entity_id in entity_registry.entities + entity = entity_registry.async_get(entity_id) + device = device_registry.async_get(entity.device_id) + assert device.name == soco_lr.get_speaker_info()["zone_name"] + + # Simulate the alarm being moved to the soco_br speaker + alarm_update = copy(alarm_clock_extended.ListAlarms.return_value) + alarm_update["CurrentAlarmList"] = alarm_update["CurrentAlarmList"].replace( + "RINCON_test", f"{soco_br.uid}" + ) + alarm_clock.ListAlarms.return_value = alarm_update + + # Update the alarm_list_version so it gets processed. + alarm_event.variables["alarm_list_version"] = "RINCON_test:1000" + alarm_update["CurrentAlarmListVersion"] = alarm_event.increment_variable( + "alarm_list_version" + ) + + alarm_clock.subscribe.return_value.callback(event=alarm_event) + await hass.async_block_till_done(wait_background_tasks=True) + + assert entity_id in entity_registry.entities + alarm_14 = entity_registry.async_get(entity_id) + device = device_registry.async_get(alarm_14.device_id) + assert device.name == soco_br.get_speaker_info()["zone_name"] diff --git a/tests/components/spotify/conftest.py b/tests/components/spotify/conftest.py index 67d4eac3960..9efc453f855 100644 --- a/tests/components/spotify/conftest.py +++ b/tests/components/spotify/conftest.py @@ -8,8 +8,8 @@ import pytest from spotifyaio.models import ( Album, Artist, - ArtistResponse, Devices, + FollowedArtistResponse, NewReleasesResponse, NewReleasesResponseInner, PlaybackState, @@ -138,7 +138,7 @@ def mock_spotify() -> Generator[AsyncMock]: getattr(client, method).return_value = obj.from_json( load_fixture(fixture, DOMAIN) ) - client.get_followed_artists.return_value = ArtistResponse.from_json( + client.get_followed_artists.return_value = FollowedArtistResponse.from_json( load_fixture("followed_artists.json", DOMAIN) ).artists.items client.get_new_releases.return_value = NewReleasesResponse.from_json( diff --git a/tests/components/spotify/snapshots/test_diagnostics.ambr b/tests/components/spotify/snapshots/test_diagnostics.ambr index 0ac375d18e3..8866fa45055 100644 --- a/tests/components/spotify/snapshots/test_diagnostics.ambr +++ b/tests/components/spotify/snapshots/test_diagnostics.ambr @@ -125,6 +125,15 @@ 'tracks': dict({ 'items': list([ dict({ + 'added_at': '2015-01-15T12:39:22+00:00', + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/jmperezperez', + }), + 'href': 'https://api.spotify.com/v1/users/jmperezperez', + 'uri': 'spotify:user:jmperezperez', + 'user_id': 'jmperezperez', + }), 'track': dict({ 'album': dict({ 'album_id': '2pANdqPvxInB0YvcDiw4ko', @@ -182,6 +191,15 @@ }), }), dict({ + 'added_at': '2015-01-15T12:40:03+00:00', + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/jmperezperez', + }), + 'href': 'https://api.spotify.com/v1/users/jmperezperez', + 'uri': 'spotify:user:jmperezperez', + 'user_id': 'jmperezperez', + }), 'track': dict({ 'album': dict({ 'album_id': '6nlfkk5GoXRL1nktlATNsy', @@ -239,6 +257,15 @@ }), }), dict({ + 'added_at': '2015-01-15T12:22:30+00:00', + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/jmperezperez', + }), + 'href': 'https://api.spotify.com/v1/users/jmperezperez', + 'uri': 'spotify:user:jmperezperez', + 'user_id': 'jmperezperez', + }), 'track': dict({ 'album': dict({ 'album_id': '4hnqM0JK4CM1phwfq1Ldyz', @@ -296,6 +323,15 @@ }), }), dict({ + 'added_at': '2015-01-15T12:40:35+00:00', + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/jmperezperez', + }), + 'href': 'https://api.spotify.com/v1/users/jmperezperez', + 'uri': 'spotify:user:jmperezperez', + 'user_id': 'jmperezperez', + }), 'track': dict({ 'album': dict({ 'album_id': '2usKFntxa98WHMcyW6xJBz', @@ -353,6 +389,15 @@ }), }), dict({ + 'added_at': '2015-01-15T12:41:10+00:00', + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/jmperezperez', + }), + 'href': 'https://api.spotify.com/v1/users/jmperezperez', + 'uri': 'spotify:user:jmperezperez', + 'user_id': 'jmperezperez', + }), 'track': dict({ 'album': dict({ 'album_id': '0ivM6kSawaug0j3tZVusG2', @@ -410,6 +455,15 @@ }), }), dict({ + 'added_at': '2024-11-28T11:20:58+00:00', + 'added_by': dict({ + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/user/1112264649', + }), + 'href': 'https://api.spotify.com/v1/users/1112264649', + 'uri': 'spotify:user:1112264649', + 'user_id': '1112264649', + }), 'track': dict({ 'description': 'Patreon: https://www.patreon.com/safetythirdMerch: https://safetythird.shopYouTube: https://www.youtube.com/@safetythird/Advertising Inquiries: https://redcircle.com/brandsPrivacy & Opt-Out: https://redcircle.com/privacy', 'duration_ms': 3690161, diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 97aca31fa05..2dd9403d53f 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -271,6 +271,7 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock: "homeassistant.components.squeezebox.Player", autospec=True ) as mock_player: mock_player.async_browse = AsyncMock(side_effect=mock_async_browse) + mock_player.async_query = AsyncMock(return_value=MagicMock()) mock_player.generate_image_url_from_track_id = MagicMock( return_value="http://lms.internal:9000/html/images/favorites.png" ) diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 5c5737804e1..89c84b1ed34 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -125,7 +125,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 81f8a93611d..a642bb44825 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -125,7 +125,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py index 411d7282893..645eb5d1ab3 100644 --- a/tests/components/switchbot/test_sensor.py +++ b/tests/components/switchbot/test_sensor.py @@ -320,13 +320,12 @@ async def test_hub2_sensor(hass: HomeAssistant) -> None: light_level_sensor_attrs = light_level_sensor.attributes assert light_level_sensor.state == "4" assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light level" - assert light_level_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "Level" - light_level_sensor = hass.states.get("sensor.test_name_illuminance") - light_level_sensor_attrs = light_level_sensor.attributes - assert light_level_sensor.state == "30" - assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Illuminance" - assert light_level_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" + illuminance_sensor = hass.states.get("sensor.test_name_illuminance") + illuminance_sensor_attrs = illuminance_sensor.attributes + assert illuminance_sensor.state == "30" + assert illuminance_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Illuminance" + assert illuminance_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal") rssi_sensor_attrs = rssi_sensor.attributes @@ -474,7 +473,6 @@ async def test_hub3_sensor(hass: HomeAssistant) -> None: light_level_sensor_attrs = light_level_sensor.attributes assert light_level_sensor.state == "3" assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light level" - assert light_level_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "Level" assert light_level_sensor_attrs[ATTR_STATE_CLASS] == "measurement" illuminance_sensor = hass.states.get("sensor.test_name_illuminance") diff --git a/tests/components/switchbot_cloud/__init__.py b/tests/components/switchbot_cloud/__init__.py index 42fe3e4f543..b0d1c29f4a9 100644 --- a/tests/components/switchbot_cloud/__init__.py +++ b/tests/components/switchbot_cloud/__init__.py @@ -1,5 +1,7 @@ """Tests for the SwitchBot Cloud integration.""" +from switchbot_api import Device + from homeassistant.components.switchbot_cloud.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant @@ -21,3 +23,20 @@ async def configure_integration(hass: HomeAssistant) -> MockConfigEntry: await hass.async_block_till_done() return entry + + +AIR_PURIFIER_INFO = Device( + version="V1.0", + deviceId="air-purifier-id-1", + deviceName="air-purifier-1", + deviceType="Air Purifier Table PM2.5", + hubDeviceId="test-hub-id", +) + +CIRCULATOR_FAN_INFO = Device( + version="V1.0", + deviceId="battery-fan-id-1", + deviceName="battery-fan-1", + deviceType="Battery Circulator Fan", + hubDeviceId="test-hub-id", +) diff --git a/tests/components/switchbot_cloud/conftest.py b/tests/components/switchbot_cloud/conftest.py index 27214fde28d..c38e3e1264e 100644 --- a/tests/components/switchbot_cloud/conftest.py +++ b/tests/components/switchbot_cloud/conftest.py @@ -39,3 +39,13 @@ def mock_after_command_refresh(): "homeassistant.components.switchbot_cloud.const.AFTER_COMMAND_REFRESH", 0 ): yield + + +@pytest.fixture(scope="package", autouse=True) +def mock_after_command_refresh_for_cover(): + """Mock after command refresh.""" + with patch( + "homeassistant.components.switchbot_cloud.const.COVER_ENTITY_AFTER_COMMAND_REFRESH", + 0, + ): + yield diff --git a/tests/components/switchbot_cloud/fixtures/air_purifier_status.json b/tests/components/switchbot_cloud/fixtures/air_purifier_status.json new file mode 100644 index 00000000000..b490c1c966c --- /dev/null +++ b/tests/components/switchbot_cloud/fixtures/air_purifier_status.json @@ -0,0 +1,8 @@ +{ + "version": "V2.3", + "power": "ON", + "mode": 2, + "deviceId": "air-purifier-id-1", + "deviceType": "Air Purifier Table PM2.5", + "hubDeviceId": "test-hub-id" +} diff --git a/tests/components/switchbot_cloud/snapshots/test_fan.ambr b/tests/components/switchbot_cloud/snapshots/test_fan.ambr new file mode 100644 index 00000000000..e5139527aca --- /dev/null +++ b/tests/components/switchbot_cloud/snapshots/test_fan.ambr @@ -0,0 +1,64 @@ +# serializer version: 1 +# name: test_air_purifier[fan.air_purifier_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + 'normal', + 'auto', + 'sleep', + 'pet', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.air_purifier_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'air_purifier', + 'unique_id': 'air-purifier-id-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_air_purifier[fan.air_purifier_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'air-purifier-1', + 'preset_mode': 'auto', + 'preset_modes': list([ + 'normal', + 'auto', + 'sleep', + 'pet', + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.air_purifier_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/switchbot_cloud/test_cover.py b/tests/components/switchbot_cloud/test_cover.py new file mode 100644 index 00000000000..0d0daf1bd7b --- /dev/null +++ b/tests/components/switchbot_cloud/test_cover.py @@ -0,0 +1,457 @@ +"""Test for the switchbot_cloud Cover.""" + +from unittest.mock import patch + +import pytest +from switchbot_api import ( + BlindTiltCommands, + CommonCommands, + CurtainCommands, + Device, + RollerShadeCommands, +) + +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.switchbot_cloud import SwitchBotAPI +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_OPEN, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +async def test_cover_set_attributes_normal( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test cover set_attributes normal.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Roller Shade", + hubDeviceId="test-hub-id", + ), + ] + + cover_id = "cover.cover_1" + mock_get_status.return_value = {"slidePosition": 100, "direction": "up"} + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_CLOSED + + +@pytest.mark.parametrize( + "device_model", + [ + "Roller Shade", + "Blind Tilt", + ], +) +async def test_cover_set_attributes_position_is_none( + hass: HomeAssistant, mock_list_devices, mock_get_status, device_model +) -> None: + """Test cover_set_attributes position is none.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType=device_model, + hubDeviceId="test-hub-id", + ), + ] + + cover_id = "cover.cover_1" + mock_get_status.side_effect = [{"direction": "up"}, {"direction": "up"}] + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "device_model", + [ + "Roller Shade", + "Blind Tilt", + ], +) +async def test_cover_set_attributes_coordinator_is_none( + hass: HomeAssistant, mock_list_devices, mock_get_status, device_model +) -> None: + """Test cover set_attributes coordinator is none.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType=device_model, + hubDeviceId="test-hub-id", + ), + ] + + cover_id = "cover.cover_1" + mock_get_status.return_value = None + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_UNKNOWN + + +async def test_curtain_features( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test curtain features.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Curtain", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "slidePosition": 95, + }, + { + "slidePosition": 95, + }, + { + "slidePosition": 95, + }, + { + "slidePosition": 95, + }, + { + "slidePosition": 95, + }, + { + "slidePosition": 95, + }, + { + "slidePosition": 95, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + cover_id = "cover.cover_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", CommonCommands.ON, "command", "default" + ) + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", CommonCommands.OFF, "command", "default" + ) + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", CurtainCommands.PAUSE, "command", "default" + ) + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {"position": 50, ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", CurtainCommands.SET_POSITION, "command", "0,ff,50" + ) + + +async def test_blind_tilt_features( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test blind_tilt features.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Blind Tilt", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + {"slidePosition": 95, "direction": "up"}, + {"slidePosition": 95, "direction": "up"}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + cover_id = "cover.cover_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", BlindTiltCommands.FULLY_OPEN, "command", "default" + ) + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", BlindTiltCommands.CLOSE_UP, "command", "default" + ) + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {"tilt_position": 25, ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", BlindTiltCommands.SET_POSITION, "command", "up;25" + ) + + +async def test_blind_tilt_features_close_down( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test blind tilt features close_down.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Blind Tilt", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + {"slidePosition": 25, "direction": "down"}, + {"slidePosition": 25, "direction": "down"}, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + cover_id = "cover.cover_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", BlindTiltCommands.CLOSE_DOWN, "command", "default" + ) + + +async def test_roller_shade_features( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test roller shade features.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Roller Shade", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "slidePosition": 95, + }, + { + "slidePosition": 95, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + cover_id = "cover.cover_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", RollerShadeCommands.SET_POSITION, "command", "0" + ) + + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_OPEN + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", RollerShadeCommands.SET_POSITION, "command", "100" + ) + + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_OPEN + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {"position": 50, ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", RollerShadeCommands.SET_POSITION, "command", "50" + ) + + +async def test_cover_set_attributes_coordinator_is_none_for_garage_door( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test cover set_attributes coordinator is none for garage_door.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Garage Door Opener", + hubDeviceId="test-hub-id", + ), + ] + cover_id = "cover.cover_1" + mock_get_status.return_value = None + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_UNKNOWN + + +async def test_garage_door_features_close( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test garage door features close.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Garage Door Opener", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "doorStatus": 1, + }, + { + "doorStatus": 1, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + cover_id = "cover.cover_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", CommonCommands.OFF, "command", "default" + ) + + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_CLOSED + + +async def test_garage_door_features_open( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test garage_door features open cover.""" + mock_list_devices.return_value = [ + Device( + version="V1.0", + deviceId="cover-id-1", + deviceName="cover-1", + deviceType="Garage Door Opener", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.side_effect = [ + { + "doorStatus": 0, + }, + { + "doorStatus": 0, + }, + ] + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + cover_id = "cover.cover_1" + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: cover_id}, + blocking=True, + ) + mock_send_command.assert_called_once_with( + "cover-id-1", CommonCommands.ON, "command", "default" + ) + + await configure_integration(hass) + state = hass.states.get(cover_id) + assert state.state == STATE_OPEN diff --git a/tests/components/switchbot_cloud/test_fan.py b/tests/components/switchbot_cloud/test_fan.py index 4a9eb527818..9852096511a 100644 --- a/tests/components/switchbot_cloud/test_fan.py +++ b/tests/components/switchbot_cloud/test_fan.py @@ -2,7 +2,10 @@ from unittest.mock import patch +import pytest +import switchbot_api from switchbot_api import Device, SwitchBotAPI +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ( ATTR_PERCENTAGE, @@ -12,6 +15,7 @@ from homeassistant.components.fan import ( SERVICE_SET_PRESET_MODE, SERVICE_TURN_ON, ) +from homeassistant.components.switchbot_cloud.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, @@ -19,32 +23,37 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from . import configure_integration +from . import AIR_PURIFIER_INFO, CIRCULATOR_FAN_INFO, configure_integration + +from tests.common import async_load_json_object_fixture, snapshot_platform +@pytest.mark.parametrize( + ("device_info", "entry_id"), + [ + (AIR_PURIFIER_INFO, "fan.air_purifier_1"), + (CIRCULATOR_FAN_INFO, "fan.battery_fan_1"), + ], +) async def test_coordinator_data_is_none( - hass: HomeAssistant, mock_list_devices, mock_get_status + hass: HomeAssistant, + mock_list_devices, + mock_get_status, + device_info: Device, + entry_id: str, ) -> None: """Test coordinator data is none.""" - mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), - ] - mock_get_status.side_effect = [ - None, - ] + mock_list_devices.return_value = [device_info] + mock_get_status.side_effect = [None] + entry = await configure_integration(hass) assert entry.state is ConfigEntryState.LOADED - entity_id = "fan.battery_fan_1" - state = hass.states.get(entity_id) + state = hass.states.get(entry_id) assert state.state == STATE_UNKNOWN @@ -52,13 +61,7 @@ async def test_coordinator_data_is_none( async def test_turn_on(hass: HomeAssistant, mock_list_devices, mock_get_status) -> None: """Test turning on the fan.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "off", "mode": "direct", "fanSpeed": "0"}, @@ -72,7 +75,9 @@ async def test_turn_on(hass: HomeAssistant, mock_list_devices, mock_get_status) assert state.state == STATE_OFF - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) @@ -87,13 +92,7 @@ async def test_turn_off( ) -> None: """Test turning off the fan.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "on", "mode": "direct", "fanSpeed": "0"}, @@ -107,7 +106,9 @@ async def test_turn_off( assert state.state == STATE_ON - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) @@ -122,13 +123,7 @@ async def test_set_percentage( ) -> None: """Test set percentage.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "on", "mode": "direct", "fanSpeed": "0"}, @@ -142,7 +137,9 @@ async def test_set_percentage( assert state.state == STATE_ON - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, @@ -157,13 +154,7 @@ async def test_set_preset_mode( ) -> None: """Test set preset mode.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "on", "mode": "direct", "fanSpeed": "0"}, @@ -177,7 +168,9 @@ async def test_set_preset_mode( assert state.state == STATE_ON - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -185,3 +178,86 @@ async def test_set_preset_mode( blocking=True, ) mock_send_command.assert_called_once() + + +async def test_air_purifier( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_list_devices, + mock_get_status, +) -> None: + """Test air purifier.""" + + mock_list_devices.return_value = [AIR_PURIFIER_INFO] + mock_get_status.return_value = await async_load_json_object_fixture( + hass, "air_purifier_status.json", DOMAIN + ) + + with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.FAN]): + entry = await configure_integration(hass) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +@pytest.mark.parametrize( + ("service", "service_data", "expected_call_args"), + [ + ( + "turn_on", + {}, + ( + "air-purifier-id-1", + switchbot_api.CommonCommands.ON, + "command", + "default", + ), + ), + ( + "turn_off", + {}, + ( + "air-purifier-id-1", + switchbot_api.CommonCommands.OFF, + "command", + "default", + ), + ), + ( + "set_preset_mode", + {"preset_mode": "sleep"}, + ( + "air-purifier-id-1", + switchbot_api.AirPurifierCommands.SET_MODE, + "command", + {"mode": 3}, + ), + ), + ], +) +async def test_air_purifier_controller( + hass: HomeAssistant, + mock_list_devices, + mock_get_status, + service: str, + service_data: dict, + expected_call_args: tuple, +) -> None: + """Test controlling the air purifier with mocked delay.""" + mock_list_devices.return_value = [AIR_PURIFIER_INFO] + mock_get_status.return_value = {"power": "OFF", "mode": 2} + + await configure_integration(hass) + fan_id = "fan.air_purifier_1" + + with ( + patch.object(SwitchBotAPI, "send_command") as mocked_send_command, + ): + await hass.services.async_call( + FAN_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: fan_id}, + blocking=True, + ) + + mocked_send_command.assert_awaited_once_with(*expected_call_args) diff --git a/tests/components/telegram_bot/test_notify.py b/tests/components/telegram_bot/test_notify.py new file mode 100644 index 00000000000..d43d5492760 --- /dev/null +++ b/tests/components/telegram_bot/test_notify.py @@ -0,0 +1,72 @@ +"""Test the telegram bot notify platform.""" + +from datetime import datetime +from unittest.mock import AsyncMock, patch + +from freezegun.api import freeze_time +from telegram import Chat, Message +from telegram.constants import ChatType, ParseMode + +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_TITLE, + DOMAIN as NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, HomeAssistant + +from tests.common import async_capture_events + + +@freeze_time("2025-01-09T12:00:00+00:00") +async def test_send_message( + hass: HomeAssistant, + webhook_platform: None, +) -> None: + """Test publishing ntfy message.""" + + context = Context() + events = async_capture_events(hass, "telegram_sent") + + with patch( + "homeassistant.components.telegram_bot.bot.Bot.send_message", + AsyncMock( + return_value=Message( + message_id=12345, + date=datetime.now(), + chat=Chat(id=123456, type=ChatType.PRIVATE), + ) + ), + ) as mock_send_message: + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + { + ATTR_ENTITY_ID: "notify.telegram_bot_123456_12345678", + ATTR_MESSAGE: "mock message", + ATTR_TITLE: "mock title", + }, + blocking=True, + context=context, + ) + await hass.async_block_till_done() + + mock_send_message.assert_called_once_with( + 12345678, + "mock title\nmock message", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=None, + disable_notification=False, + reply_to_message_id=None, + reply_markup=None, + read_timeout=None, + message_thread_id=None, + ) + + state = hass.states.get("notify.telegram_bot_123456_12345678") + assert state + assert state.state == "2025-01-09T12:00:00+00:00" + + assert len(events) == 1 + assert events[0].context == context diff --git a/tests/components/template/snapshots/test_event.ambr b/tests/components/template/snapshots/test_event.ambr new file mode 100644 index 00000000000..98ec3ffa8c0 --- /dev/null +++ b/tests/components/template/snapshots/test_event.ambr @@ -0,0 +1,20 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'single', + 'event_types': list([ + 'single', + 'double', + 'hold', + ]), + 'friendly_name': 'template_event', + }), + 'context': , + 'entity_id': 'event.template_event', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-07-09T00:00:00.000+00:00', + }) +# --- diff --git a/tests/components/template/snapshots/test_update.ambr b/tests/components/template/snapshots/test_update.ambr new file mode 100644 index 00000000000..479ccb88ffc --- /dev/null +++ b/tests/components/template/snapshots/test_update.ambr @@ -0,0 +1,26 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/template/icon.png', + 'friendly_name': 'template_update', + 'in_progress': False, + 'installed_version': '1.0', + 'latest_version': '2.0', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.template_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index c1df654e328..319d02a1056 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -973,3 +973,35 @@ async def test_optimistic(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == AlarmControlPanelState.ARMED_HOME + + +@pytest.mark.parametrize( + ("count", "panel_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ states('alarm_control_panel.test') }}", + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_panel") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_arm_away", + {"entity_id": TEST_ENTITY_ID, "code": "1234"}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 08104025582..3bf7b836a8b 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -149,6 +149,16 @@ BINARY_SENSOR_OPTIONS = { }, {}, ), + ( + "event", + {"event_type": "{{ states('event.one') }}"}, + "2024-07-09T00:00:00.000+00:00", + {"one": "single", "two": "double"}, + {}, + {"event_types": "{{ ['single', 'double'] }}"}, + {"event_types": "{{ ['single', 'double'] }}"}, + {}, + ), ( "fan", {"state": "{{ states('fan.one') }}"}, @@ -239,6 +249,16 @@ BINARY_SENSOR_OPTIONS = { {}, {}, ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + "off", + {"one": "2.0", "two": "1.0"}, + {}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + {}, + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, @@ -362,6 +382,12 @@ async def test_config_flow( {"set_cover_position": []}, {"set_cover_position": []}, ), + ( + "event", + {"event_type": "{{ 'single' }}"}, + {"event_types": "{{ ['single', 'double'] }}"}, + {"event_types": "{{ ['single', 'double'] }}"}, + ), ( "fan", {"state": "{{ states('fan.one') }}"}, @@ -424,6 +450,12 @@ async def test_config_flow( {"options": "{{ ['off', 'on', 'auto'] }}"}, {"options": "{{ ['off', 'on', 'auto'] }}"}, ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, @@ -582,6 +614,16 @@ async def test_config_flow_device( {"set_cover_position": []}, "state", ), + ( + "event", + {"event_type": "{{ states('event.one') }}"}, + {"event_type": "{{ states('event.two') }}"}, + ["2024-07-09T00:00:00.000+00:00", "2024-07-09T00:00:00.000+00:00"], + {"one": "single", "two": "double"}, + {"event_types": "{{ ['single', 'double'] }}"}, + {"event_types": "{{ ['single', 'double'] }}"}, + "event_type", + ), ( "fan", {"state": "{{ states('fan.one') }}"}, @@ -689,6 +731,16 @@ async def test_config_flow_device( {}, "value_template", ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + {"installed_version": "{{ states('update.two') }}"}, + ["off", "on"], + {"one": "2.0", "two": "1.0"}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + "installed_version", + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, @@ -1469,6 +1521,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {"set_cover_position": []}, {"set_cover_position": []}, ), + ( + "event", + {"event_type": "{{ 'single' }}"}, + {"event_types": "{{ ['single', 'double'] }}"}, + {"event_types": "{{ ['single', 'double'] }}"}, + ), ( "fan", {"state": "{{ states('fan.one') }}"}, @@ -1538,6 +1596,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 692567c7aa8..2a83967b048 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -628,11 +628,38 @@ async def test_template_position( ], ) @pytest.mark.usefixtures("setup_cover") -async def test_template_not_optimistic(hass: HomeAssistant) -> None: +async def test_template_not_optimistic( + hass: HomeAssistant, + calls: list[ServiceCall], +) -> None: """Test the is_closed attribute.""" state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN + # Test to make sure optimistic is not set with only a position template. + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + # Test to make sure optimistic is not set with only a position template. + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + @pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) @pytest.mark.parametrize( diff --git a/tests/components/template/test_event.py b/tests/components/template/test_event.py new file mode 100644 index 00000000000..4efa488a66a --- /dev/null +++ b/tests/components/template/test_event.py @@ -0,0 +1,823 @@ +"""The tests for the Template event platform.""" + +from typing import Any + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import event, template +from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ATTR_ICON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import ConfigurationStyle, async_get_flow_preview_state + +from tests.common import ( + MockConfigEntry, + assert_setup_component, + mock_restore_cache_with_extra_data, +) +from tests.conftest import WebSocketGenerator + +TEST_OBJECT_ID = "template_event" +TEST_ENTITY_ID = f"event.{TEST_OBJECT_ID}" +TEST_SENSOR = "sensor.event" +TEST_STATE_TRIGGER = { + "trigger": {"trigger": "state", "entity_id": TEST_SENSOR}, + "variables": {"triggering_entity": "{{ trigger.entity_id }}"}, + "action": [ + {"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}} + ], +} +TEST_EVENT_TYPES_TEMPLATE = "{{ ['single', 'double', 'hold'] }}" +TEST_EVENT_TYPE_TEMPLATE = "{{ 'single' }}" + +TEST_EVENT_CONFIG = { + "event_types": TEST_EVENT_TYPES_TEMPLATE, + "event_type": TEST_EVENT_TYPE_TEMPLATE, +} +TEST_UNIQUE_ID_CONFIG = { + **TEST_EVENT_CONFIG, + "unique_id": "not-so-unique-anymore", +} +TEST_FROZEN_INPUT = "2024-07-09 00:00:00+00:00" +TEST_FROZEN_STATE = "2024-07-09T00:00:00.000+00:00" + + +async def async_setup_modern_format( + hass: HomeAssistant, + count: int, + event_config: dict[str, Any], + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of event integration via new format.""" + extra = extra_config if extra_config else {} + config = {**event_config, **extra} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + {"template": {"event": config}}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_trigger_format( + hass: HomeAssistant, + count: int, + event_config: dict[str, Any], + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of event integration via trigger format.""" + extra = extra_config if extra_config else {} + config = { + "template": { + **TEST_STATE_TRIGGER, + "event": {**event_config, **extra}, + } + } + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_event_config( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + event_config: dict[str, Any], + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of event integration.""" + if style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, event_config, extra_config) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format(hass, count, event_config, extra_config) + + +@pytest.fixture +async def setup_base_event( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + event_config: dict[str, Any], +) -> None: + """Do setup of event integration.""" + await async_setup_event_config( + hass, + count, + style, + event_config, + None, + ) + + +@pytest.fixture +async def setup_event( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + event_type_template: str, + event_types_template: str, + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of event integration.""" + await async_setup_event_config( + hass, + count, + style, + { + "name": TEST_OBJECT_ID, + "event_type": event_type_template, + "event_types": event_types_template, + }, + extra_config, + ) + + +@pytest.fixture +async def setup_single_attribute_state_event( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + event_type_template: str, + event_types_template: str, + attribute: str, + attribute_template: str, +) -> None: + """Do setup of event integration testing a single attribute.""" + extra = {attribute: attribute_template} if attribute and attribute_template else {} + config = { + "name": TEST_OBJECT_ID, + "event_type": event_type_template, + "event_types": event_types_template, + } + if style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, config, extra) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format(hass, count, config, extra) + + +async def test_legacy_platform_config(hass: HomeAssistant) -> None: + """Test a legacy platform does not create event entities.""" + with assert_setup_component(1, event.DOMAIN): + assert await async_setup_component( + hass, + event.DOMAIN, + {"event": {"platform": "template", "events": {TEST_OBJECT_ID: {}}}}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + assert hass.states.async_all("event") == [] + + +@pytest.mark.freeze_time(TEST_FROZEN_INPUT) +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test the config flow.""" + + hass.states.async_set( + TEST_SENSOR, + "single", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": TEST_OBJECT_ID, + "event_type": TEST_EVENT_TYPE_TEMPLATE, + "event_types": TEST_EVENT_TYPES_TEMPLATE, + "template_type": event.DOMAIN, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state == snapshot + + +@pytest.mark.freeze_time(TEST_FROZEN_INPUT) +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for device for Template.""" + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("test", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "event_type": TEST_EVENT_TYPE_TEMPLATE, + "event_types": TEST_EVENT_TYPES_TEMPLATE, + "template_type": "event", + "device_id": device_entry.id, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + template_entity = entity_registry.async_get("event.my_template") + assert template_entity is not None + assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("count", "event_types_template", "extra_config"), + [(1, TEST_EVENT_TYPES_TEMPLATE, None)], +) +@pytest.mark.parametrize( + ("style", "expected_state"), + [ + (ConfigurationStyle.MODERN, STATE_UNKNOWN), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.parametrize("event_type_template", ["{{states.test['big.fat...']}}"]) +@pytest.mark.usefixtures("setup_event") +async def test_event_type_syntax_error( + hass: HomeAssistant, + expected_state: str, +) -> None: + """Test template event_type with render error.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("count", "event_type_template", "event_types_template", "extra_config"), + [(1, "{{ states('sensor.event') }}", TEST_EVENT_TYPES_TEMPLATE, None)], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + ("event", "expected"), + [ + ("single", "single"), + ("double", "double"), + ("hold", "hold"), + ], +) +@pytest.mark.usefixtures("setup_event") +async def test_event_type_template( + hass: HomeAssistant, + event: str, + expected: str, +) -> None: + """Test template event_type.""" + hass.states.async_set(TEST_SENSOR, event) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["event_type"] == expected + + +@pytest.mark.parametrize( + ("count", "event_type_template", "event_types_template", "extra_config"), + [(1, "{{ states('sensor.event') }}", TEST_EVENT_TYPES_TEMPLATE, None)], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_event") +@pytest.mark.freeze_time(TEST_FROZEN_INPUT) +async def test_event_type_template_updates( + hass: HomeAssistant, +) -> None: + """Test template event_type updates.""" + hass.states.async_set(TEST_SENSOR, "single") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == TEST_FROZEN_STATE + assert state.attributes["event_type"] == "single" + + hass.states.async_set(TEST_SENSOR, "double") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == TEST_FROZEN_STATE + assert state.attributes["event_type"] == "double" + + hass.states.async_set(TEST_SENSOR, "hold") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == TEST_FROZEN_STATE + assert state.attributes["event_type"] == "hold" + + +@pytest.mark.parametrize( + ("count", "event_types_template", "extra_config"), + [(1, TEST_EVENT_TYPES_TEMPLATE, None)], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + "event_type_template", + [ + "{{ None }}", + "{{ 7 }}", + "{{ 'unknown' }}", + "{{ 'tripple_double' }}", + ], +) +@pytest.mark.usefixtures("setup_event") +async def test_event_type_invalid( + hass: HomeAssistant, +) -> None: + """Test template event_type.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + assert state.attributes["event_type"] is None + + +@pytest.mark.parametrize( + ("count", "event_type_template", "event_types_template"), + [(1, "{{ states('sensor.event') }}", TEST_EVENT_TYPES_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + ("attribute", "attribute_template", "key", "expected"), + [ + ( + "picture", + "{% if is_state('sensor.event', 'double') %}something{% endif %}", + ATTR_ENTITY_PICTURE, + "something", + ), + ( + "icon", + "{% if is_state('sensor.event', 'double') %}mdi:something{% endif %}", + ATTR_ICON, + "mdi:something", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_event") +async def test_entity_picture_and_icon_templates( + hass: HomeAssistant, key: str, expected: str +) -> None: + """Test picture and icon template.""" + state = hass.states.async_set(TEST_SENSOR, "single") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(key) in ("", None) + + state = hass.states.async_set(TEST_SENSOR, "double") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + + assert state.attributes[key] == expected + + +@pytest.mark.parametrize( + ("count", "event_type_template", "extra_config"), + [(1, "{{ None }}", None)], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + ("event_types_template", "expected"), + [ + ( + "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", + ["Strobe color", "Police", "Christmas", "RGB", "Random Loop"], + ), + ( + "{{ ['Police', 'RGB', 'Random Loop'] }}", + ["Police", "RGB", "Random Loop"], + ), + ("{{ [] }}", []), + ("{{ '[]' }}", []), + ("{{ 124 }}", []), + ("{{ '124' }}", []), + ("{{ none }}", []), + ("", []), + ], +) +@pytest.mark.usefixtures("setup_event") +async def test_event_types_template(hass: HomeAssistant, expected: str) -> None: + """Test template event_types.""" + hass.states.async_set(TEST_SENSOR, "anything") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["event_types"] == expected + + +@pytest.mark.parametrize( + ("count", "event_type_template", "event_types_template", "extra_config"), + [ + ( + 1, + "{{ states('sensor.event') }}", + "{{ state_attr('sensor.event', 'options') or ['unknown'] }}", + None, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_event") +@pytest.mark.freeze_time(TEST_FROZEN_INPUT) +async def test_event_types_template_updates(hass: HomeAssistant) -> None: + """Test template event_type update with entity.""" + hass.states.async_set( + TEST_SENSOR, "single", {"options": ["single", "double", "hold"]} + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == TEST_FROZEN_STATE + assert state.attributes["event_type"] == "single" + assert state.attributes["event_types"] == ["single", "double", "hold"] + + hass.states.async_set(TEST_SENSOR, "double", {"options": ["double", "hold"]}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == TEST_FROZEN_STATE + assert state.attributes["event_type"] == "double" + assert state.attributes["event_types"] == ["double", "hold"] + + +@pytest.mark.parametrize( + ( + "count", + "event_type_template", + "event_types_template", + "attribute", + "attribute_template", + ), + [ + ( + 1, + "{{ states('sensor.event') }}", + TEST_EVENT_TYPES_TEMPLATE, + "availability", + "{{ states('sensor.event') in ['single', 'double', 'hold'] }}", + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_single_attribute_state_event") +async def test_available_template_with_entities(hass: HomeAssistant) -> None: + """Test availability templates with values from other entities.""" + hass.states.async_set(TEST_SENSOR, "single") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state != STATE_UNAVAILABLE + assert state.attributes["event_type"] == "single" + + hass.states.async_set(TEST_SENSOR, "triple") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + assert "event_type" not in state.attributes + + +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "event": { + "name": TEST_OBJECT_ID, + "event_type": "{{ trigger.event.data.action }}", + "event_types": TEST_EVENT_TYPES_TEMPLATE, + "picture": "{{ '/local/dogs.png' }}", + "icon": "{{ 'mdi:pirate' }}", + "attributes": { + "plus_one": "{{ trigger.event.data.beer + 1 }}", + "plus_two": "{{ trigger.event.data.beer + 2 }}", + }, + }, + }, + }, + ], +) +async def test_trigger_entity_restore_state( + hass: HomeAssistant, + count: int, + domain: str, + config: dict, +) -> None: + """Test restoring trigger event entities.""" + restored_attributes = { + "entity_picture": "/local/cats.png", + "event_type": "hold", + "icon": "mdi:ship", + "plus_one": 55, + } + fake_state = State( + TEST_ENTITY_ID, + "2021-01-01T23:59:59.123+00:00", + restored_attributes, + ) + fake_extra_data = { + "last_event_type": "hold", + "last_event_attributes": restored_attributes, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + test_state = "2021-01-01T23:59:59.123+00:00" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == test_state + for attr, value in restored_attributes.items(): + assert state.attributes[attr] == value + assert "plus_two" not in state.attributes + + hass.bus.async_fire("test_event", {"action": "double", "beer": 2}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state != test_state + assert state.attributes["icon"] == "mdi:pirate" + assert state.attributes["entity_picture"] == "/local/dogs.png" + assert state.attributes["event_type"] == "double" + assert state.attributes["event_types"] == ["single", "double", "hold"] + assert state.attributes["plus_one"] == 3 + assert state.attributes["plus_two"] == 4 + + +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "event": { + "name": TEST_OBJECT_ID, + "event_type": "{{ states('sensor.event') }}", + "event_types": TEST_EVENT_TYPES_TEMPLATE, + }, + }, + }, + ], +) +async def test_event_entity_restore_state( + hass: HomeAssistant, + count: int, + domain: str, + config: dict, +) -> None: + """Test restoring trigger event entities.""" + fake_state = State( + TEST_ENTITY_ID, + "2021-01-01T23:59:59.123+00:00", + {}, + ) + fake_extra_data = { + "last_event_type": "hold", + "last_event_attributes": {}, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + test_state = "2021-01-01T23:59:59.123+00:00" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == test_state + + hass.states.async_set(TEST_SENSOR, "double") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state != test_state + assert state.attributes["event_type"] == "double" + + +@pytest.mark.parametrize( + ( + "count", + "event_type_template", + "event_types_template", + "attribute", + "attribute_template", + ), + [ + ( + 1, + TEST_EVENT_TYPE_TEMPLATE, + TEST_EVENT_TYPES_TEMPLATE, + "availability", + "{{ x - 12 }}", + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_single_attribute_state_event") +async def test_invalid_availability_template_keeps_component_available( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + caplog_setup_text, +) -> None: + """Test that an invalid availability keeps the device available.""" + hass.states.async_set(TEST_SENSOR, "anything") + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE + + error = "UndefinedError: 'x' is undefined" + assert error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("events", "style"), + [ + ( + [ + { + "name": "test_template_event_01", + **TEST_UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_event_02", + **TEST_UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.MODERN, + ), + ( + [ + { + "name": "test_template_event_01", + **TEST_UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_event_02", + **TEST_UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.TRIGGER, + ), + ], +) +async def test_unique_id( + hass: HomeAssistant, count: int, events: list[dict], style: ConfigurationStyle +) -> None: + """Test unique_id option only creates one event per id.""" + config = {"event": events} + if style == ConfigurationStyle.TRIGGER: + config = {**config, **TEST_STATE_TRIGGER} + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + {"template": config}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("event")) == 1 + + +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test unique_id option creates one event per nested id.""" + + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "event": [ + { + "name": "test_a", + **TEST_EVENT_CONFIG, + "unique_id": "a", + }, + { + "name": "test_b", + **TEST_EVENT_CONFIG, + "unique_id": "b", + }, + ], + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("event")) == 2 + + entry = entity_registry.async_get("event.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("event.test_b") + assert entry + assert entry.unique_id == "x-b" + + +@pytest.mark.freeze_time(TEST_FROZEN_INPUT) +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + event.DOMAIN, + {"name": "My template", **TEST_EVENT_CONFIG}, + ) + + assert state["state"] == TEST_FROZEN_STATE + assert state["attributes"]["event_type"] == "single" + assert state["attributes"]["event_types"] == ["single", "double", "hold"] diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b9161edf61a..81486d75137 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -1885,6 +1885,39 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: assert state.state == STATE_OFF +@pytest.mark.parametrize( + ("count", "fan_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('sensor.test_sensor', 'on') }}", + "turn_on": [], + "turn_off": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_fan") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + fan.DOMAIN, + "turn_on", + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 0d593da9fba..a95bf2a6332 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -364,6 +364,30 @@ async def async_yaml_patch_helper(hass: HomeAssistant, filename: str) -> None: "value_template": "{{ true }}", }, ), + ( + { + "template_type": "event", + "name": "My template", + "event_type": "{{ 'single' }}", + "event_types": "{{ ['single', 'double'] }}", + }, + { + "event_type": "{{ 'single' }}", + "event_types": "{{ ['single', 'double'] }}", + }, + ), + ( + { + "template_type": "update", + "name": "My template", + "latest_version": "{{ '1.0' }}", + "installed_version": "{{ '1.0' }}", + }, + { + "latest_version": "{{ '1.0' }}", + "installed_version": "{{ '1.0' }}", + }, + ), ], ) async def test_change_device( diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 0549f9981e7..e5d05cfa08f 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -2795,6 +2795,42 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: assert state.state == STATE_OFF +@pytest.mark.parametrize( + ("count", "light_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('light.test_state', 'on') }}", + "turn_on": [], + "turn_off": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + ("style", "expected"), + [ + (ConfigurationStyle.MODERN, STATE_OFF), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.usefixtures("setup_light") +async def test_not_optimistic(hass: HomeAssistant, expected: str) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + light.DOMAIN, + "turn_on", + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 823306015bf..6a4164fb802 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -1190,6 +1190,39 @@ async def test_optimistic(hass: HomeAssistant) -> None: assert state.state == LockState.UNLOCKED +@pytest.mark.parametrize( + ("count", "lock_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('sensor.test_state', 'on') }}", + "lock": [], + "unlock": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_lock") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_LOCK, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == LockState.UNLOCKED + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 0ae98a23ae4..f10664e0d5f 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -605,6 +605,37 @@ async def test_optimistic(hass: HomeAssistant) -> None: assert float(state.state) == 2 +@pytest.mark.parametrize( + ("count", "number_config"), + [ + ( + 1, + { + "state": "{{ states('sensor.test_state') }}", + "optimistic": False, + "set_value": [], + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_number") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: _TEST_NUMBER, "value": 4}, + blocking=True, + ) + + state = hass.states.get(_TEST_NUMBER) + assert state.state == STATE_UNKNOWN + + @pytest.mark.parametrize( ("count", "number_config"), [ diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index f613fa865a6..eda27f18100 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -601,6 +601,42 @@ async def test_optimistic(hass: HomeAssistant) -> None: assert state.state == "yes" +@pytest.mark.parametrize( + ("count", "select_config"), + [ + ( + 1, + { + "state": "{{ states('select.test_state') }}", + "optimistic": False, + "options": "{{ ['test', 'yes', 'no'] }}", + "select_option": [], + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.usefixtures("setup_select") +async def test_not_optimistic(hass: HomeAssistant) -> None: + """Test optimistic yaml option set to false.""" + # Ensure Trigger template entities update the options list + hass.states.async_set(TEST_STATE_ENTITY_ID, "anything") + await hass.async_block_till_done() + + await hass.services.async_call( + select.DOMAIN, + select.SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: _TEST_SELECT, "option": "test"}, + blocking=True, + ) + + state = hass.states.get(_TEST_SELECT) + assert state.state == STATE_UNKNOWN + + @pytest.mark.parametrize( ("count", "select_config"), [ diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index a32f1df4c76..5a884160fe8 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -1267,3 +1268,39 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + ("count", "switch_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ is_state('switch.test_state', 'on') }}", + "turn_on": [], + "turn_off": [], + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + ("style", "expected"), + [ + (ConfigurationStyle.MODERN, STATE_OFF), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.usefixtures("setup_switch") +async def test_not_optimistic(hass: HomeAssistant, expected: str) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + switch.DOMAIN, + "turn_on", + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected diff --git a/tests/components/template/test_update.py b/tests/components/template/test_update.py new file mode 100644 index 00000000000..61fbfeede7a --- /dev/null +++ b/tests/components/template/test_update.py @@ -0,0 +1,1085 @@ +"""The tests for the Template update platform.""" + +from typing import Any + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import template, update +from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ATTR_ICON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, ServiceCall, State +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import ( + ConfigurationStyle, + async_get_flow_preview_state, + async_setup_modern_state_format, + async_setup_modern_trigger_format, + make_test_trigger, +) + +from tests.common import ( + MockConfigEntry, + assert_setup_component, + mock_restore_cache_with_extra_data, +) +from tests.conftest import WebSocketGenerator + +TEST_OBJECT_ID = "template_update" +TEST_ENTITY_ID = f"update.{TEST_OBJECT_ID}" +TEST_INSTALLED_SENSOR = "sensor.installed_update" +TEST_LATEST_SENSOR = "sensor.latest_update" +TEST_SENSOR_ID = "sensor.test_update" +TEST_STATE_TRIGGER = make_test_trigger( + TEST_INSTALLED_SENSOR, TEST_LATEST_SENSOR, TEST_SENSOR_ID +) +TEST_INSTALLED_TEMPLATE = "{{ '1.0' }}" +TEST_LATEST_TEMPLATE = "{{ '2.0' }}" + +TEST_UPDATE_CONFIG = { + "installed_version": TEST_INSTALLED_TEMPLATE, + "latest_version": TEST_LATEST_TEMPLATE, +} +TEST_UNIQUE_ID_CONFIG = { + **TEST_UPDATE_CONFIG, + "unique_id": "not-so-unique-anymore", +} + +INSTALL_ACTION = { + "install": { + "action": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "install", + "backup": "{{ backup }}", + "specific_version": "{{ specific_version }}", + }, + } +} + + +async def async_setup_config( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + config: dict[str, Any], + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of update integration.""" + config = {**config, **extra_config} if extra_config else config + if style == ConfigurationStyle.MODERN: + await async_setup_modern_state_format(hass, update.DOMAIN, count, config) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_modern_trigger_format( + hass, update.DOMAIN, TEST_STATE_TRIGGER, count, config + ) + + +@pytest.fixture +async def setup_base( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + config: dict[str, Any], +) -> None: + """Do setup of update integration.""" + await async_setup_config( + hass, + count, + style, + config, + None, + ) + + +@pytest.fixture +async def setup_update( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + installed_template: str, + latest_template: str, + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of update integration.""" + await async_setup_config( + hass, + count, + style, + { + "name": TEST_OBJECT_ID, + "installed_version": installed_template, + "latest_version": latest_template, + }, + extra_config, + ) + + +@pytest.fixture +async def setup_single_attribute_update( + hass: HomeAssistant, + style: ConfigurationStyle, + installed_template: str, + latest_template: str, + attribute: str, + attribute_template: str, +) -> None: + """Do setup of update platform testing a single attribute.""" + await async_setup_config( + hass, + 1, + style, + { + "name": TEST_OBJECT_ID, + "installed_version": installed_template, + "latest_version": latest_template, + }, + {attribute: attribute_template} if attribute and attribute_template else {}, + ) + + +async def test_legacy_platform_config(hass: HomeAssistant) -> None: + """Test a legacy platform does not create update entities.""" + with assert_setup_component(1, update.DOMAIN): + assert await async_setup_component( + hass, + update.DOMAIN, + {"update": {"platform": "template", "updates": {TEST_OBJECT_ID: {}}}}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + assert hass.states.async_all("update") == [] + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test the config flow.""" + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": TEST_OBJECT_ID, + "template_type": update.DOMAIN, + **TEST_UPDATE_CONFIG, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state == snapshot + + +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for device for Template.""" + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("test", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": TEST_OBJECT_ID, + "template_type": update.DOMAIN, + **TEST_UPDATE_CONFIG, + "device_id": device_entry.id, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + template_entity = entity_registry.async_get(TEST_ENTITY_ID) + assert template_entity is not None + assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize(("count", "extra_config"), [(1, None)]) +@pytest.mark.parametrize( + ("style", "expected_state"), + [ + (ConfigurationStyle.MODERN, STATE_UNKNOWN), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.parametrize( + ("installed_template", "latest_template"), + [ + ("{{states.test['big.fat...']}}", TEST_LATEST_TEMPLATE), + (TEST_INSTALLED_TEMPLATE, "{{states.test['big.fat...']}}"), + ("{{states.test['big.fat...']}}", "{{states.test['big.fat...']}}"), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_syntax_error( + hass: HomeAssistant, + expected_state: str, +) -> None: + """Test template update with render error.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template", "latest_template"), + [ + ( + 1, + None, + "{{ states('sensor.installed_update') }}", + "{{ states('sensor.latest_update') }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("installed", "latest", "expected"), + [ + ("1.0", "2.0", STATE_ON), + ("2.0", "2.0", STATE_OFF), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_update_templates( + hass: HomeAssistant, installed: str, latest: str, expected: str +) -> None: + """Test update template.""" + hass.states.async_set(TEST_INSTALLED_SENSOR, installed) + hass.states.async_set(TEST_LATEST_SENSOR, latest) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == expected + assert state.attributes["installed_version"] == installed + assert state.attributes["latest_version"] == latest + + # ensure that the entity picture exists when not provided. + assert ( + state.attributes["entity_picture"] + == "https://brands.home-assistant.io/_/template/icon.png" + ) + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template", "latest_template"), + [ + ( + 1, + None, + "{{ states('sensor.installed_update') }}", + "{{ states('sensor.latest_update') }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_update") +async def test_installed_and_latest_template_updates_from_entity( + hass: HomeAssistant, +) -> None: + """Test template installed and latest version templates updates from entities.""" + hass.states.async_set(TEST_INSTALLED_SENSOR, "1.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "1.0" + assert state.attributes["latest_version"] == "2.0" + + hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "2.0" + assert state.attributes["latest_version"] == "2.0" + + hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0") + hass.states.async_set(TEST_LATEST_SENSOR, "3.0") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "2.0" + assert state.attributes["latest_version"] == "3.0" + + +@pytest.mark.parametrize( + ("count", "extra_config", "latest_template"), + [(1, None, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("installed_template", "expected", "expected_attr"), + [ + ("{{ '1.0' }}", STATE_ON, "1.0"), + ("{{ 1.0 }}", STATE_ON, "1.0"), + ("{{ '2.0' }}", STATE_OFF, "2.0"), + ("{{ 2.0 }}", STATE_OFF, "2.0"), + ("{{ None }}", STATE_UNKNOWN, None), + ("{{ 'foo' }}", STATE_ON, "foo"), + ("{{ x + 2 }}", STATE_UNKNOWN, None), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_installed_version_template( + hass: HomeAssistant, expected: str, expected_attr: Any +) -> None: + """Test installed_version template results.""" + # Ensure trigger based template entities update + hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == expected + assert state.attributes["installed_version"] == expected_attr + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template"), + [(1, None, TEST_INSTALLED_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("latest_template", "expected", "expected_attr"), + [ + ("{{ '1.0' }}", STATE_OFF, "1.0"), + ("{{ 1.0 }}", STATE_OFF, "1.0"), + ("{{ '2.0' }}", STATE_ON, "2.0"), + ("{{ 2.0 }}", STATE_ON, "2.0"), + ("{{ None }}", STATE_UNKNOWN, None), + ("{{ 'foo' }}", STATE_ON, "foo"), + ("{{ x + 2 }}", STATE_UNKNOWN, None), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_latest_version_template( + hass: HomeAssistant, expected: str, expected_attr: Any +) -> None: + """Test latest_version template results.""" + # Ensure trigger based template entities update + hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == expected + assert state.attributes["latest_version"] == expected_attr + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template", "latest_template"), + [ + ( + 1, + INSTALL_ACTION, + "{{ states('sensor.installed_update') }}", + "{{ states('sensor.latest_update') }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_update") +async def test_install_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: + """Test install action.""" + + hass.states.async_set(TEST_INSTALLED_SENSOR, "1.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + await hass.services.async_call( + update.DOMAIN, + update.SERVICE_INSTALL, + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + assert calls[-1].data["action"] == "install" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + # Ensure an error is raised when there's no update. + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + update.DOMAIN, + update.SERVICE_INSTALL, + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + assert calls[-1].data["action"] == "install" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + +@pytest.mark.parametrize( + ("installed_template", "latest_template"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute", "attribute_template", "key", "expected"), + [ + ( + "picture", + "{% if is_state('sensor.installed_update', 'on') %}something{% endif %}", + ATTR_ENTITY_PICTURE, + "something", + ), + ( + "icon", + "{% if is_state('sensor.installed_update', 'on') %}mdi:something{% endif %}", + ATTR_ICON, + "mdi:something", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_entity_picture_and_icon_templates( + hass: HomeAssistant, key: str, expected: str +) -> None: + """Test picture and icon template.""" + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(key) in ("", None) + + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + + assert state.attributes[key] == expected + + +@pytest.mark.parametrize( + ("installed_template", "latest_template"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute", "attribute_template"), + [ + ( + "picture", + "{{ 'foo.png' if is_state('sensor.installed_update', 'on') else None }}", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_entity_picture_uses_default(hass: HomeAssistant) -> None: + """Test entity picture when template resolves None.""" + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes[ATTR_ENTITY_PICTURE] == "foo.png" + + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + + assert ( + state.attributes[ATTR_ENTITY_PICTURE] + == "https://brands.home-assistant.io/_/template/icon.png" + ) + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "in_progress")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute_template", "expected", "error"), + [ + ("{{ True }}", True, None), + ("{{ False }}", False, None), + ("{{ None }}", False, "Received invalid in_process value: None"), + ( + "{{ 'foo' }}", + False, + "Received invalid in_process value: foo", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_in_process_template( + hass: HomeAssistant, + attribute: str, + expected: Any, + error: str | None, + caplog: pytest.LogCaptureFixture, + caplog_setup_text: str, +) -> None: + """Test in process templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + assert error is None or error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize( + ( + "installed_template", + "latest_template", + ), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize("attribute", ["release_summary", "title"]) +@pytest.mark.parametrize( + ("attribute_template", "expected"), + [ + ("{{ True }}", "True"), + ("{{ False }}", "False"), + ("{{ None }}", None), + ("{{ 'foo' }}", "foo"), + ("{{ 1.0 }}", "1.0"), + ("{{ x + 2 }}", None), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_release_summary_and_title_templates( + hass: HomeAssistant, + attribute: str, + expected: Any, +) -> None: + """Test release summary and title templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "release_url")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute_template", "expected", "error"), + [ + ("{{ 'http://foo.bar' }}", "http://foo.bar", None), + ("{{ 'https://foo.bar' }}", "https://foo.bar", None), + ("{{ None }}", None, None), + ( + "{{ '/local/thing' }}", + None, + "Received invalid release_url: /local/thing", + ), + ( + "{{ 'foo' }}", + None, + "Received invalid release_url: foo", + ), + ( + "{{ 1.0 }}", + None, + "Received invalid release_url: 1", + ), + ( + "{{ True }}", + None, + "Received invalid release_url: True", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_release_url_template( + hass: HomeAssistant, + attribute: str, + expected: Any, + error: str | None, + caplog: pytest.LogCaptureFixture, + caplog_setup_text: str, +) -> None: + """Test release url templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + assert error is None or error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "update_percentage")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute_template", "expected", "error"), + [ + ("{{ 100 }}", 100, None), + ("{{ 0 }}", 0, None), + ("{{ 45 }}", 45, None), + ("{{ None }}", None, None), + ("{{ -1 }}", None, "Received invalid update_percentage: -1"), + ("{{ 101 }}", None, "Received invalid update_percentage: 101"), + ("{{ 'foo' }}", None, "Received invalid update_percentage: foo"), + ("{{ x - 4 }}", None, "UndefinedError: 'x' is undefined"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_update_percent_template( + hass: HomeAssistant, + attribute: str, + expected: Any, + error: str | None, + caplog: pytest.LogCaptureFixture, + caplog_setup_text: str, +) -> None: + """Test update percent templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + assert error is None or error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute", "attribute_template"), + [ + ( + TEST_INSTALLED_TEMPLATE, + TEST_LATEST_TEMPLATE, + "update_percentage", + "{% set e = 'sensor.test_update' %}{{ states(e) if e | has_value else None }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_optimistic_in_progress_with_update_percent_template( + hass: HomeAssistant, +) -> None: + """Test optimistic in_progress attribute with update percent templates.""" + # Ensure trigger entities trigger. + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["in_progress"] is False + assert state.attributes["update_percentage"] is None + + for i in range(101): + state = hass.states.async_set(TEST_SENSOR_ID, i) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["in_progress"] is True + assert state.attributes["update_percentage"] == i + + state = hass.states.async_set(TEST_SENSOR_ID, STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["in_progress"] is False + assert state.attributes["update_percentage"] is None + + +@pytest.mark.parametrize( + ( + "count", + "installed_template", + "latest_template", + ), + [(1, TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ( + "extra_config", + "supported_feature", + "action_data", + "expected_backup", + "expected_version", + ), + [ + ( + {"backup": True, **INSTALL_ACTION}, + update.UpdateEntityFeature.BACKUP | update.UpdateEntityFeature.INSTALL, + {"backup": True}, + True, + None, + ), + ( + {"specific_version": True, **INSTALL_ACTION}, + update.UpdateEntityFeature.SPECIFIC_VERSION + | update.UpdateEntityFeature.INSTALL, + {"version": "v2.0"}, + False, + "v2.0", + ), + ( + {"backup": True, "specific_version": True, **INSTALL_ACTION}, + update.UpdateEntityFeature.SPECIFIC_VERSION + | update.UpdateEntityFeature.BACKUP + | update.UpdateEntityFeature.INSTALL, + {"backup": True, "version": "v2.0"}, + True, + "v2.0", + ), + (INSTALL_ACTION, update.UpdateEntityFeature.INSTALL, {}, False, None), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_supported_features( + hass: HomeAssistant, + supported_feature: update.UpdateEntityFeature, + action_data: dict, + calls: list[ServiceCall], + expected_backup: bool, + expected_version: str | None, +) -> None: + """Test release summary and title templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["supported_features"] == supported_feature + + await hass.services.async_call( + update.DOMAIN, + update.SERVICE_INSTALL, + {"entity_id": TEST_ENTITY_ID, **action_data}, + blocking=True, + ) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + data = calls[-1].data + assert data["action"] == "install" + assert data["caller"] == TEST_ENTITY_ID + assert data["backup"] == expected_backup + assert data["specific_version"] == expected_version + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute", "attribute_template"), + [ + ( + TEST_INSTALLED_TEMPLATE, + TEST_LATEST_TEMPLATE, + "availability", + "{{ 'sensor.test_update' | has_value }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_available_template_with_entities(hass: HomeAssistant) -> None: + """Test availability templates with values from other entities.""" + hass.states.async_set(TEST_SENSOR_ID, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state != STATE_UNAVAILABLE + + hass.states.async_set(TEST_SENSOR_ID, STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set(TEST_SENSOR_ID, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state != STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute", "attribute_template"), + [ + ( + TEST_INSTALLED_TEMPLATE, + TEST_LATEST_TEMPLATE, + "availability", + "{{ x - 12 }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_invalid_availability_template_keeps_component_available( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + caplog_setup_text, +) -> None: + """Test that an invalid availability keeps the device available.""" + # Ensure entity triggers + hass.states.async_set(TEST_SENSOR_ID, "anything") + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE + + error = "UndefinedError: 'x' is undefined" + assert error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "update": { + "name": TEST_OBJECT_ID, + "installed_version": "{{ trigger.event.data.action }}", + "latest_version": "{{ '1.0.2' }}", + "picture": "{{ '/local/dogs.png' }}", + "icon": "{{ 'mdi:pirate' }}", + }, + }, + }, + ], +) +async def test_trigger_entity_restore_state( + hass: HomeAssistant, + count: int, + domain: str, + config: dict, +) -> None: + """Test restoring trigger entities.""" + restored_attributes = { + "installed_version": "1.0.0", + "latest_version": "1.0.1", + "entity_picture": "/local/cats.png", + "icon": "mdi:ship", + "skipped_version": "1.0.1", + } + fake_state = State( + TEST_ENTITY_ID, + STATE_OFF, + restored_attributes, + ) + mock_restore_cache_with_extra_data(hass, ((fake_state, {}),)) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + for attr, value in restored_attributes.items(): + assert state.attributes[attr] == value + + hass.bus.async_fire("test_event", {"action": "1.0.0"}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + assert state.attributes["icon"] == "mdi:pirate" + assert state.attributes["entity_picture"] == "/local/dogs.png" + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("updates", "style"), + [ + ( + [ + { + "name": "test_template_event_01", + **TEST_UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_event_02", + **TEST_UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.MODERN, + ), + ( + [ + { + "name": "test_template_event_01", + **TEST_UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_event_02", + **TEST_UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.TRIGGER, + ), + ], +) +async def test_unique_id( + hass: HomeAssistant, count: int, updates: list[dict], style: ConfigurationStyle +) -> None: + """Test unique_id option only creates one update entity per id.""" + config = {"update": updates} + if style == ConfigurationStyle.TRIGGER: + config = {**config, **TEST_STATE_TRIGGER} + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + {"template": config}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("update")) == 1 + + +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test unique_id option creates one update entity per nested id.""" + + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "update": [ + { + "name": "test_a", + **TEST_UPDATE_CONFIG, + "unique_id": "a", + }, + { + "name": "test_b", + **TEST_UPDATE_CONFIG, + "unique_id": "b", + }, + ], + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("update")) == 2 + + entry = entity_registry.async_get("update.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("update.test_b") + assert entry + assert entry.unique_id == "x-b" + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + update.DOMAIN, + {"name": "My template", **TEST_UPDATE_CONFIG}, + ) + + assert state["state"] == STATE_ON + assert state["attributes"]["installed_version"] == "1.0" + assert state["attributes"]["latest_version"] == "2.0" diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 8c2773956b2..21592718551 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -1299,6 +1299,54 @@ async def test_optimistic_option( assert state.state == VacuumActivity.DOCKED +@pytest.mark.parametrize( + ("count", "vacuum_config"), + [ + ( + 1, + { + "name": TEST_OBJECT_ID, + "state": "{{ states('sensor.test_state') }}", + "start": [], + **TEMPLATE_VACUUM_ACTIONS, + "optimistic": False, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + "service", + [ + vacuum.SERVICE_START, + vacuum.SERVICE_PAUSE, + vacuum.SERVICE_STOP, + vacuum.SERVICE_RETURN_TO_BASE, + vacuum.SERVICE_CLEAN_SPOT, + ], +) +@pytest.mark.usefixtures("setup_vacuum") +async def test_not_optimistic( + hass: HomeAssistant, + service: str, + calls: list[ServiceCall], +) -> None: + """Test optimistic yaml option set to false.""" + await hass.services.async_call( + vacuum.DOMAIN, + service, + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNKNOWN + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index e177865d2f9..00e8d54c9fe 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -1,6 +1,6 @@ """Test the Teslemetry init.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -11,6 +11,7 @@ from tesla_fleet_api.exceptions import ( TeslaFleetError, ) +from homeassistant.components.teslemetry.const import DOMAIN from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL from homeassistant.components.teslemetry.models import TeslemetryData from homeassistant.config_entries import ConfigEntryState @@ -187,3 +188,94 @@ async def test_modern_no_poll( assert mock_vehicle_data.called is False freezer.tick(VEHICLE_INTERVAL) assert mock_vehicle_data.called is False + + +async def test_stale_device_removal( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_products: AsyncMock, +) -> None: + """Test removal of stale devices.""" + + # Setup the entry first to get a valid config_entry_id + entry = await setup_platform(hass) + + # Create a device that should be removed (with the valid entry_id) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "stale-vin")}, + manufacturer="Tesla", + name="Stale Vehicle", + ) + + # Verify the stale device exists + pre_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + stale_identifiers = { + identifier for device in pre_devices for identifier in device.identifiers + } + assert (DOMAIN, "stale-vin") in stale_identifiers + + # Update products with an empty response (no devices) and reload entry + with patch( + "tesla_fleet_api.teslemetry.Teslemetry.products", + return_value={"response": []}, + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + # Get updated devices after reload + post_devices = dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ) + post_identifiers = { + identifier for device in post_devices for identifier in device.identifiers + } + + # Verify the stale device has been removed + assert (DOMAIN, "stale-vin") not in post_identifiers + + # Verify the device itself has been completely removed from the registry + # since it had no other config entries + updated_device = device_registry.async_get_device( + identifiers={(DOMAIN, "stale-vin")} + ) + assert updated_device is None + + +async def test_device_retention_during_reload( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_products: AsyncMock, +) -> None: + """Test that valid devices are retained during a config entry reload.""" + # Setup entry with normal devices + entry = await setup_platform(hass) + + # Get initial device count and identifiers + pre_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + pre_count = len(pre_devices) + pre_identifiers = { + identifier for device in pre_devices for identifier in device.identifiers + } + + # Make sure we have some devices + assert pre_count > 0 + + # Save the original identifiers to compare after reload + original_identifiers = pre_identifiers.copy() + + # Reload the config entry with the same products data + # The mock_products fixture will return the same data as during setup + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + # Verify device count and identifiers after reload match pre-reload + post_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + post_count = len(post_devices) + post_identifiers = { + identifier for device in post_devices for identifier in device.identifiers + } + + # Since the products data didn't change, we should have the same devices + assert post_count == pre_count + assert post_identifiers == original_identifiers diff --git a/tests/components/teslemetry/test_services.py b/tests/components/teslemetry/test_services.py index bcf5407999f..fecb8db0092 100644 --- a/tests/components/teslemetry/test_services.py +++ b/tests/components/teslemetry/test_services.py @@ -1,23 +1,36 @@ """Test the Teslemetry services.""" +from datetime import time from unittest.mock import patch import pytest from homeassistant.components.teslemetry.const import DOMAIN from homeassistant.components.teslemetry.services import ( + ATTR_DAYS_OF_WEEK, ATTR_DEPARTURE_TIME, ATTR_ENABLE, ATTR_END_OFF_PEAK_TIME, + ATTR_END_TIME, ATTR_GPS, + ATTR_ID, + ATTR_LOCATION, + ATTR_NAME, ATTR_OFF_PEAK_CHARGING_ENABLED, ATTR_OFF_PEAK_CHARGING_WEEKDAYS, + ATTR_ONE_TIME, ATTR_PIN, + ATTR_PRECONDITION_TIME, ATTR_PRECONDITIONING_ENABLED, ATTR_PRECONDITIONING_WEEKDAYS, + ATTR_START_TIME, ATTR_TIME, ATTR_TOU_SETTINGS, + SERVICE_ADD_CHARGE_SCHEDULE, + SERVICE_ADD_PRECONDITION_SCHEDULE, SERVICE_NAVIGATE_ATTR_GPS_REQUEST, + SERVICE_REMOVE_CHARGE_SCHEDULE, + SERVICE_REMOVE_PRECONDITION_SCHEDULE, SERVICE_SET_SCHEDULED_CHARGING, SERVICE_SET_SCHEDULED_DEPARTURE, SERVICE_SPEED_LIMIT, @@ -75,23 +88,12 @@ async def test_services( { CONF_DEVICE_ID: vehicle_device, ATTR_ENABLE: True, - ATTR_TIME: "6:00", + ATTR_TIME: "06:00", # 6:00 AM }, blocking=True, ) set_scheduled_charging.assert_called_once() - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_SCHEDULED_CHARGING, - { - CONF_DEVICE_ID: vehicle_device, - ATTR_ENABLE: True, - }, - blocking=True, - ) - with patch( "tesla_fleet_api.teslemetry.Vehicle.set_scheduled_departure", return_value=COMMAND_OK, @@ -104,39 +106,15 @@ async def test_services( ATTR_ENABLE: True, ATTR_PRECONDITIONING_ENABLED: True, ATTR_PRECONDITIONING_WEEKDAYS: False, - ATTR_DEPARTURE_TIME: "6:00", + ATTR_DEPARTURE_TIME: "06:00", # 6:00 AM ATTR_OFF_PEAK_CHARGING_ENABLED: True, ATTR_OFF_PEAK_CHARGING_WEEKDAYS: False, - ATTR_END_OFF_PEAK_TIME: "5:00", + ATTR_END_OFF_PEAK_TIME: "05:00", # 5:00 AM }, blocking=True, ) set_scheduled_departure.assert_called_once() - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_SCHEDULED_DEPARTURE, - { - CONF_DEVICE_ID: vehicle_device, - ATTR_ENABLE: True, - ATTR_PRECONDITIONING_ENABLED: True, - }, - blocking=True, - ) - - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_SCHEDULED_DEPARTURE, - { - CONF_DEVICE_ID: vehicle_device, - ATTR_ENABLE: True, - ATTR_OFF_PEAK_CHARGING_ENABLED: True, - }, - blocking=True, - ) - with patch( "tesla_fleet_api.teslemetry.Vehicle.set_valet_mode", return_value=COMMAND_OK, @@ -200,6 +178,112 @@ async def test_services( ) set_time_of_use.assert_called_once() + with patch( + "tesla_fleet_api.teslemetry.Vehicle.add_charge_schedule", + return_value=COMMAND_OK, + ) as add_charge_schedule: + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_CHARGE_SCHEDULE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_DAYS_OF_WEEK: ["Monday", "Tuesday"], + ATTR_ENABLE: True, + ATTR_LOCATION: {CONF_LATITUDE: lat, CONF_LONGITUDE: lon}, + ATTR_START_TIME: time(7, 0, 0), # 7:00 AM + ATTR_END_TIME: time(18, 0, 0), # 6:00 PM + ATTR_ONE_TIME: False, + ATTR_NAME: "Test Schedule", + }, + blocking=True, + ) + add_charge_schedule.assert_called_once() + + # Test add_charge_schedule with minimal required parameters + with patch( + "tesla_fleet_api.teslemetry.Vehicle.add_charge_schedule", + return_value=COMMAND_OK, + ) as add_charge_schedule_minimal: + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_CHARGE_SCHEDULE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_DAYS_OF_WEEK: ["Monday", "Tuesday"], + ATTR_ENABLE: True, + }, + blocking=True, + ) + add_charge_schedule_minimal.assert_called_once() + + with patch( + "tesla_fleet_api.teslemetry.Vehicle.remove_charge_schedule", + return_value=COMMAND_OK, + ) as remove_charge_schedule: + await hass.services.async_call( + DOMAIN, + SERVICE_REMOVE_CHARGE_SCHEDULE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ID: 123, + }, + blocking=True, + ) + remove_charge_schedule.assert_called_once() + + with patch( + "tesla_fleet_api.teslemetry.Vehicle.add_precondition_schedule", + return_value=COMMAND_OK, + ) as add_precondition_schedule: + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRECONDITION_SCHEDULE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_DAYS_OF_WEEK: ["Monday", "Tuesday"], + ATTR_ENABLE: True, + ATTR_LOCATION: {CONF_LATITUDE: lat, CONF_LONGITUDE: lon}, + ATTR_PRECONDITION_TIME: time(7, 0, 0), # 7:00 AM + ATTR_ONE_TIME: False, + ATTR_NAME: "Test Precondition Schedule", + }, + blocking=True, + ) + add_precondition_schedule.assert_called_once() + + # Test add_precondition_schedule with minimal required parameters + with patch( + "tesla_fleet_api.teslemetry.Vehicle.add_precondition_schedule", + return_value=COMMAND_OK, + ) as add_precondition_schedule_minimal: + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_PRECONDITION_SCHEDULE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_DAYS_OF_WEEK: ["Monday", "Tuesday"], + ATTR_ENABLE: True, + ATTR_PRECONDITION_TIME: time(8, 0, 0), # 8:00 AM + }, + blocking=True, + ) + add_precondition_schedule_minimal.assert_called_once() + + with patch( + "tesla_fleet_api.teslemetry.Vehicle.remove_precondition_schedule", + return_value=COMMAND_OK, + ) as remove_precondition_schedule: + await hass.services.async_call( + DOMAIN, + SERVICE_REMOVE_PRECONDITION_SCHEDULE, + { + CONF_DEVICE_ID: vehicle_device, + ATTR_ID: 123, + }, + blocking=True, + ) + remove_precondition_schedule.assert_called_once() + with ( patch( "tesla_fleet_api.teslemetry.EnergySite.time_of_use_settings", diff --git a/tests/components/togrill/__init__.py b/tests/components/togrill/__init__.py new file mode 100644 index 00000000000..9e0d164ae2a --- /dev/null +++ b/tests/components/togrill/__init__.py @@ -0,0 +1,40 @@ +"""Tests for the ToGrill Bluetooth integration.""" + +from unittest.mock import patch + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from tests.common import MockConfigEntry + +TOGRILL_SERVICE_INFO = BluetoothServiceInfo( + name="Pro-05", + address="00000000-0000-0000-0000-000000000001", + rssi=-63, + service_data={}, + manufacturer_data={34714: b"\xd9\xe3\xbe\xf3\x00"}, + service_uuids=["0000cee0-0000-1000-8000-00805f9b34fb"], + source="local", +) + +TOGRILL_SERVICE_INFO_NO_NAME = BluetoothServiceInfo( + name="", + address="00000000-0000-0000-0000-000000000002", + rssi=-63, + service_data={}, + manufacturer_data={34714: b"\xd9\xe3\xbe\xf3\x00"}, + service_uuids=["0000cee0-0000-1000-8000-00805f9b34fb"], + source="local", +) + + +async def setup_entry( + hass: HomeAssistant, mock_entry: MockConfigEntry, platforms: list[Platform] +) -> None: + """Make sure the device is available.""" + + with patch("homeassistant.components.togrill._PLATFORMS", platforms): + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/togrill/conftest.py b/tests/components/togrill/conftest.py new file mode 100644 index 00000000000..6b028ca5270 --- /dev/null +++ b/tests/components/togrill/conftest.py @@ -0,0 +1,96 @@ +"""Common fixtures for the ToGrill tests.""" + +from collections.abc import Callable, Generator +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from togrill_bluetooth.client import Client +from togrill_bluetooth.packets import Packet, PacketA0Notify, PacketNotify + +from homeassistant.components.togrill.const import CONF_PROBE_COUNT, DOMAIN +from homeassistant.const import CONF_ADDRESS, CONF_MODEL + +from . import TOGRILL_SERVICE_INFO + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_entry() -> MockConfigEntry: + """Create hass config fixture.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: TOGRILL_SERVICE_INFO.address, + CONF_MODEL: "Pro-05", + CONF_PROBE_COUNT: 2, + }, + unique_id=TOGRILL_SERVICE_INFO.address, + ) + + +@pytest.fixture(scope="module") +def mock_unload_entry() -> Generator[AsyncMock]: + """Override async_unload_entry.""" + with patch( + "homeassistant.components.togrill.async_unload_entry", + return_value=True, + ) as mock_unload_entry: + yield mock_unload_entry + + +@pytest.fixture(scope="module") +def mock_setup_entry(mock_unload_entry) -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.togrill.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(autouse=True) +def mock_client(enable_bluetooth: None, mock_client_class: Mock) -> Generator[Mock]: + """Auto mock bluetooth.""" + + client_object = Mock(spec=Client) + client_object.mocked_notify = None + + async def _connect( + address: str, callback: Callable[[Packet], None] | None = None + ) -> Mock: + client_object.mocked_notify = callback + return client_object + + async def _disconnect() -> None: + pass + + async def _request(packet_type: type[Packet]) -> None: + if packet_type is PacketA0Notify: + client_object.mocked_notify(PacketA0Notify(0, 0, 0, 0, 0, False, 0, False)) + + async def _read(packet_type: type[PacketNotify]) -> PacketNotify: + if packet_type is PacketA0Notify: + return PacketA0Notify(0, 0, 0, 0, 0, False, 0, False) + raise NotImplementedError + + mock_client_class.connect.side_effect = _connect + client_object.request.side_effect = _request + client_object.read.side_effect = _read + client_object.disconnect.side_effect = _disconnect + client_object.is_connected = True + + return client_object + + +@pytest.fixture(autouse=True) +def mock_client_class() -> Generator[Mock]: + """Auto mock bluetooth.""" + + with ( + patch( + "homeassistant.components.togrill.config_flow.Client", autospec=True + ) as client_class, + patch("homeassistant.components.togrill.coordinator.Client", new=client_class), + ): + yield client_class diff --git a/tests/components/togrill/snapshots/test_event.ambr b/tests/components/togrill/snapshots/test_event.ambr new file mode 100644 index 00000000000..99908cd85c2 --- /dev/null +++ b/tests/components/togrill/snapshots/test_event.ambr @@ -0,0 +1,469 @@ +# serializer version: 1 +# name: test_setup[no_data][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[no_data][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'device_low_power', + 'device_high_temp', + 'probe_below_minimum', + 'probe_above_maximum', + 'probe_alarm', + 'probe_disconnected', + 'ignition_failure', + 'ambient_low_temp', + 'ambient_over_heat', + 'ambient_cool_down', + 'probe_timer_alarm', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/togrill/snapshots/test_init.ambr b/tests/components/togrill/snapshots/test_init.ambr new file mode 100644 index 00000000000..b461d103e73 --- /dev/null +++ b/tests/components/togrill/snapshots/test_init.ambr @@ -0,0 +1,32 @@ +# serializer version: 1 +# name: test_setup_device_present + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'bluetooth', + '00000000-0000-0000-0000-000000000001', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + }), + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': 'Pro-05', + 'name': 'Pro-05', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': '0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/togrill/snapshots/test_number.ambr b/tests/components/togrill/snapshots/test_number.ambr new file mode 100644 index 00000000000..639f2758c69 --- /dev/null +++ b/tests/components/togrill/snapshots/test_number.ambr @@ -0,0 +1,355 @@ +# serializer version: 1 +# name: test_setup[no_data][number.pro_05_alarm_interval-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 15, + 'min': 0, + 'mode': , + 'step': 5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.pro_05_alarm_interval', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alarm interval', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_interval', + 'unique_id': '00000000-0000-0000-0000-000000000001_alarm_interval', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][number.pro_05_alarm_interval-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pro-05 Alarm interval', + 'max': 15, + 'min': 0, + 'mode': , + 'step': 5, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pro_05_alarm_interval', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[no_data][number.pro_05_target_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.pro_05_target_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_target', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][number.pro_05_target_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Target 1', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pro_05_target_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[no_data][number.pro_05_target_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.pro_05_target_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_target', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][number.pro_05_target_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Target 2', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pro_05_target_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.pro_05_alarm_interval-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 15, + 'min': 0, + 'mode': , + 'step': 5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.pro_05_alarm_interval', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Alarm interval', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_interval', + 'unique_id': '00000000-0000-0000-0000-000000000001_alarm_interval', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.pro_05_alarm_interval-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pro-05 Alarm interval', + 'max': 15, + 'min': 0, + 'mode': , + 'step': 5, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pro_05_alarm_interval', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.pro_05_target_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_target', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Target 1', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pro_05_target_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.0', + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.pro_05_target_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_target', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_target_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[one_probe_with_target_alarm][number.pro_05_target_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Target 2', + 'max': 250, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pro_05_target_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/togrill/snapshots/test_sensor.ambr b/tests/components/togrill/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..bc55d831500 --- /dev/null +++ b/tests/components/togrill/snapshots/test_sensor.ambr @@ -0,0 +1,673 @@ +# serializer version: 1 +# name: test_setup[battery][sensor.pro_05_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pro_05_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000-0000-0000-0000-000000000001_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[battery][sensor.pro_05_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Pro-05 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.pro_05_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45', + }) +# --- +# name: test_setup[battery][sensor.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[battery][sensor.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_setup[battery][sensor.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[battery][sensor.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_setup[no_data][sensor.pro_05_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pro_05_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000-0000-0000-0000-000000000001_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[no_data][sensor.pro_05_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Pro-05 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.pro_05_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[no_data][sensor.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][sensor.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_setup[no_data][sensor.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[no_data][sensor.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_setup[temp_data][sensor.pro_05_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pro_05_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000-0000-0000-0000-000000000001_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[temp_data][sensor.pro_05_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Pro-05 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.pro_05_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[temp_data][sensor.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[temp_data][sensor.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_setup[temp_data][sensor.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[temp_data][sensor.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_setup[temp_data_missing_probe][sensor.pro_05_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pro_05_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000-0000-0000-0000-000000000001_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[temp_data_missing_probe][sensor.pro_05_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Pro-05 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.pro_05_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_1', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': '00000000-0000-0000-0000-000000000001_temperature_2', + 'unit_of_measurement': , + }) +# --- +# name: test_setup[temp_data_missing_probe][sensor.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pro-05 Probe 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/togrill/test_config_flow.py b/tests/components/togrill/test_config_flow.py new file mode 100644 index 00000000000..2620a88f7f2 --- /dev/null +++ b/tests/components/togrill/test_config_flow.py @@ -0,0 +1,155 @@ +"""Test the ToGrill config flow.""" + +from unittest.mock import Mock + +from bleak.exc import BleakError +import pytest + +from homeassistant import config_entries +from homeassistant.components.togrill.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import TOGRILL_SERVICE_INFO, TOGRILL_SERVICE_INFO_NO_NAME, setup_entry + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_user_selection( + hass: HomeAssistant, +) -> None: + """Test we can select a device.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO_NO_NAME) + await hass.async_block_till_done(wait_background_tasks=True) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": TOGRILL_SERVICE_INFO.address}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + "address": TOGRILL_SERVICE_INFO.address, + "model": "Pro-05", + "probe_count": 0, + } + assert result["title"] == "Pro-05" + assert result["result"].unique_id == TOGRILL_SERVICE_INFO.address + + +async def test_failed_connect( + hass: HomeAssistant, + mock_client: Mock, + mock_client_class: Mock, +) -> None: + """Test failure to connect result.""" + + mock_client_class.connect.side_effect = BleakError("Failed to connect") + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": TOGRILL_SERVICE_INFO.address}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "failed_to_read_config" + + +async def test_failed_read( + hass: HomeAssistant, + mock_client: Mock, +) -> None: + """Test failure to read from device.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + mock_client.read.side_effect = BleakError("something went wrong") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": TOGRILL_SERVICE_INFO.address}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "failed_to_read_config" + + +async def test_no_devices( + hass: HomeAssistant, +) -> None: + """Test missing device.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO_NO_NAME) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_duplicate_setup( + hass: HomeAssistant, + mock_entry: MockConfigEntry, +) -> None: + """Test we can not setup a device again.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + await hass.async_block_till_done(wait_background_tasks=True) + + await setup_entry(hass, mock_entry, []) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_bluetooth( + hass: HomeAssistant, +) -> None: + """Test bluetooth device discovery.""" + + # Inject the service info will trigger the flow to start + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + await hass.async_block_till_done(wait_background_tasks=True) + + result = next(iter(hass.config_entries.flow.async_progress_by_handler(DOMAIN))) + + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + "address": TOGRILL_SERVICE_INFO.address, + "model": "Pro-05", + "probe_count": 0, + } + assert result["title"] == "Pro-05" + assert result["result"].unique_id == TOGRILL_SERVICE_INFO.address diff --git a/tests/components/togrill/test_event.py b/tests/components/togrill/test_event.py new file mode 100644 index 00000000000..6aa6019303a --- /dev/null +++ b/tests/components/togrill/test_event.py @@ -0,0 +1,78 @@ +"""Test events for ToGrill integration.""" + +from unittest.mock import Mock + +import pytest +from syrupy.assertion import SnapshotAssertion +from togrill_bluetooth.packets import PacketA1Notify, PacketA5Notify + +from homeassistant.components.event import ATTR_EVENT_TYPE +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util import slugify + +from . import TOGRILL_SERVICE_INFO, setup_entry + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.freeze_time("2023-10-21") +@pytest.mark.parametrize( + "packets", + [ + pytest.param([], id="no_data"), + pytest.param([PacketA1Notify([10, None])], id="non_event_packet"), + pytest.param([PacketA5Notify(probe=1, message=99)], id="non_known_message"), + ], +) +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + packets, +) -> None: + """Test standard events.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.EVENT]) + + for packet in packets: + mock_client.mocked_notify(packet) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) + + +@pytest.mark.freeze_time("2023-10-21") +@pytest.mark.parametrize( + "message", + [pytest.param(message, id=message.name) for message in PacketA5Notify.Message], +) +async def test_events( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + message: PacketA5Notify.Message, +) -> None: + """Test all possible events.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.EVENT]) + + mock_client.mocked_notify(PacketA5Notify(probe=1, message=message)) + + state = hass.states.get("event.pro_05_probe_2") + assert state + assert state.state == STATE_UNKNOWN + + state = hass.states.get("event.pro_05_probe_1") + assert state + assert state.state == "2023-10-21T00:00:00.000+00:00" + assert state.attributes.get(ATTR_EVENT_TYPE) == slugify(message.name) diff --git a/tests/components/togrill/test_init.py b/tests/components/togrill/test_init.py new file mode 100644 index 00000000000..7f441817176 --- /dev/null +++ b/tests/components/togrill/test_init.py @@ -0,0 +1,67 @@ +"""Test for initialization of ToGrill integration.""" + +from unittest.mock import Mock + +from bleak.exc import BleakError +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import TOGRILL_SERVICE_INFO, setup_entry + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_setup_device_present( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_client_class: Mock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that setup works with device present.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, []) + assert mock_entry.state is ConfigEntryState.LOADED + + device = device_registry.async_get_device( + connections={(dr.CONNECTION_BLUETOOTH, TOGRILL_SERVICE_INFO.address)} + ) + assert device == snapshot + + +async def test_setup_device_not_present( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_client_class: Mock, +) -> None: + """Test that setup succeeds if device is missing.""" + + await setup_entry(hass, mock_entry, []) + assert mock_entry.state is ConfigEntryState.LOADED + + +async def test_setup_device_failing( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + mock_client_class: Mock, +) -> None: + """Test that setup fails if device is not responding.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + mock_client.is_connected = False + mock_client.read.side_effect = BleakError("Failed to read data") + + await setup_entry(hass, mock_entry, []) + assert mock_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/togrill/test_number.py b/tests/components/togrill/test_number.py new file mode 100644 index 00000000000..f6031e114d1 --- /dev/null +++ b/tests/components/togrill/test_number.py @@ -0,0 +1,243 @@ +"""Test numbers for ToGrill integration.""" + +from unittest.mock import Mock + +from bleak.exc import BleakError +import pytest +from syrupy.assertion import SnapshotAssertion +from togrill_bluetooth.exceptions import BaseError +from togrill_bluetooth.packets import ( + PacketA0Notify, + PacketA6Write, + PacketA8Notify, + PacketA301Write, +) + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import TOGRILL_SERVICE_INFO, setup_entry + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.parametrize( + "packets", + [ + pytest.param([], id="no_data"), + pytest.param( + [ + PacketA0Notify( + battery=45, + version_major=1, + version_minor=5, + function_type=1, + probe_count=2, + ambient=False, + alarm_interval=5, + alarm_sound=True, + ), + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + temperature_1=50.0, + ), + PacketA8Notify(probe=2, alarm_type=None), + ], + id="one_probe_with_target_alarm", + ), + ], +) +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + packets, +) -> None: + """Test the numbers.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.NUMBER]) + + for packet in packets: + mock_client.mocked_notify(packet) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) + + +@pytest.mark.parametrize( + ("packets", "entity_id", "value", "write_packet"), + [ + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + temperature_1=50.0, + ), + ], + "number.pro_05_target_1", + 100.0, + PacketA301Write(probe=1, target=100), + id="probe", + ), + pytest.param( + [ + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + temperature_1=50.0, + ), + ], + "number.pro_05_target_1", + 0.0, + PacketA301Write(probe=1, target=None), + id="probe_clear", + ), + pytest.param( + [ + PacketA0Notify( + battery=45, + version_major=1, + version_minor=5, + function_type=1, + probe_count=2, + ambient=False, + alarm_interval=5, + alarm_sound=True, + ) + ], + "number.pro_05_alarm_interval", + 15, + PacketA6Write(temperature_unit=None, alarm_interval=15), + id="alarm_interval", + ), + ], +) +async def test_set_number( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, + packets, + entity_id, + value, + write_packet, +) -> None: + """Test the number set.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.NUMBER]) + + for packet in packets: + mock_client.mocked_notify(packet) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_VALUE: value, + }, + target={ + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + mock_client.write.assert_any_call(write_packet) + + +@pytest.mark.parametrize( + ("error", "message"), + [ + pytest.param( + BleakError("Some error"), + "Communication failed with the device", + id="bleak", + ), + pytest.param( + BaseError("Some error"), + "Data was rejected by device", + id="base", + ), + ], +) +async def test_set_number_write_error( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, + error, + message, +) -> None: + """Test the number set.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.NUMBER]) + + mock_client.mocked_notify( + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + temperature_1=50.0, + ), + ) + mock_client.write.side_effect = error + + with pytest.raises(HomeAssistantError, match=message): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_VALUE: 100, + }, + target={ + ATTR_ENTITY_ID: "number.pro_05_target_1", + }, + blocking=True, + ) + + +async def test_set_number_disconnected( + hass: HomeAssistant, + mock_entry: MockConfigEntry, + mock_client: Mock, +) -> None: + """Test the number set.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.NUMBER]) + + mock_client.mocked_notify( + PacketA8Notify( + probe=1, + alarm_type=PacketA8Notify.AlarmType.TEMPERATURE_TARGET, + temperature_1=50.0, + ), + ) + mock_client.is_connected = False + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_VALUE: 100, + }, + target={ + ATTR_ENTITY_ID: "number.pro_05_target_1", + }, + blocking=True, + ) diff --git a/tests/components/togrill/test_sensor.py b/tests/components/togrill/test_sensor.py new file mode 100644 index 00000000000..d7662d483af --- /dev/null +++ b/tests/components/togrill/test_sensor.py @@ -0,0 +1,59 @@ +"""Test sensors for ToGrill integration.""" + +from unittest.mock import Mock + +import pytest +from syrupy.assertion import SnapshotAssertion +from togrill_bluetooth.packets import PacketA0Notify, PacketA1Notify + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import TOGRILL_SERVICE_INFO, setup_entry + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.parametrize( + "packets", + [ + pytest.param([], id="no_data"), + pytest.param( + [ + PacketA0Notify( + battery=45, + version_major=1, + version_minor=5, + function_type=1, + probe_count=2, + ambient=False, + alarm_interval=5, + alarm_sound=True, + ) + ], + id="battery", + ), + pytest.param([PacketA1Notify([10, None])], id="temp_data"), + pytest.param([PacketA1Notify([10])], id="temp_data_missing_probe"), + ], +) +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + packets, +) -> None: + """Test the sensors.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.SENSOR]) + + for packet in packets: + mock_client.mocked_notify(packet) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) diff --git a/tests/components/totalconnect/__init__.py b/tests/components/totalconnect/__init__.py index 180a00188cd..e7b358157cb 100644 --- a/tests/components/totalconnect/__init__.py +++ b/tests/components/totalconnect/__init__.py @@ -1 +1,13 @@ """Tests for the totalconnect component.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py deleted file mode 100644 index 34d451ec0b8..00000000000 --- a/tests/components/totalconnect/common.py +++ /dev/null @@ -1,473 +0,0 @@ -"""Common methods used across tests for TotalConnect.""" - -from typing import Any -from unittest.mock import patch - -from total_connect_client import ArmingState, ResultCode, ZoneStatus, ZoneType - -from homeassistant.components.totalconnect.const import ( - AUTO_BYPASS, - CODE_REQUIRED, - CONF_USERCODES, - DOMAIN, -) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry - -LOCATION_ID = 123456 - -DEVICE_INFO_BASIC_1 = { - "DeviceID": "987654", - "DeviceName": "test", - "DeviceClassID": 1, - "DeviceSerialNumber": "987654321ABC", - "DeviceFlags": "PromptForUserCode=0,PromptForInstallerCode=0,PromptForImportSecuritySettings=0,AllowUserSlotEditing=0,CalCapable=1,CanBeSentToPanel=0,CanArmNightStay=0,CanSupportMultiPartition=0,PartitionCount=0,MaxPartitionCount=0,OnBoardingSupport=0,PartitionAdded=0,DuplicateUserSyncStatus=0,PanelType=8,PanelVariant=1,BLEDisarmCapable=0,ArmHomeSupported=0,DuplicateUserCodeCheck=1,CanSupportRapid=0,IsKeypadSupported=1,WifiEnrollmentSupported=0,IsConnectedPanel=0,ArmNightInSceneSupported=0,BuiltInCameraSettingsSupported=0,ZWaveThermostatScheduleDisabled=0,MultipleAuthorityLevelSupported=0,VideoOnPanelSupported=0,EnableBLEMode=0,IsPanelWiFiResetSupported=0,IsCompetitorClearBypass=0,IsNotReadyStateSupported=0,isArmStatusWithoutExitDelayNotSupported=0", - "SecurityPanelTypeID": None, - "DeviceSerialText": None, -} -DEVICE_LIST = [DEVICE_INFO_BASIC_1] - -LOCATION_INFO_BASIC_NORMAL = { - "LocationID": LOCATION_ID, - "LocationName": "test", - "SecurityDeviceID": "987654", - "PhotoURL": "http://www.example.com/some/path/to/file.jpg", - "LocationModuleFlags": "Security=1,Video=0,Automation=0,GPS=0,VideoPIR=0", - "DeviceList": {"DeviceInfoBasic": DEVICE_LIST}, -} - -LOCATIONS = {"LocationInfoBasic": [LOCATION_INFO_BASIC_NORMAL]} - -MODULE_FLAGS = "Some=0,Fake=1,Flags=2" - -USER = { - "UserID": "1234567", - "Username": "username", - "UserFeatureList": "Master=0,User Administration=0,Configuration Administration=0", -} - -RESPONSE_SESSION_DETAILS = { - "ResultCode": ResultCode.SUCCESS.value, - "ResultData": "Success", - "SessionID": "12345", - "Locations": LOCATIONS, - "ModuleFlags": MODULE_FLAGS, - "UserInfo": USER, -} - -PARTITION_DISARMED = { - "PartitionID": "1", - "ArmingState": ArmingState.DISARMED, -} - -PARTITION_DISARMED2 = { - "PartitionID": "2", - "ArmingState": ArmingState.DISARMED, -} - -PARTITION_ARMED_STAY = { - "PartitionID": "1", - "ArmingState": ArmingState.ARMED_STAY, -} - -PARTITION_ARMED_STAY2 = { - "PartitionID": "2", - "ArmingState": ArmingState.DISARMED, -} - -PARTITION_ARMED_AWAY = { - "PartitionID": "1", - "ArmingState": ArmingState.ARMED_AWAY, -} - -PARTITION_ARMED_CUSTOM = { - "PartitionID": "1", - "ArmingState": ArmingState.ARMED_CUSTOM_BYPASS, -} - -PARTITION_ARMED_NIGHT = { - "PartitionID": "1", - "ArmingState": ArmingState.ARMED_STAY_NIGHT, -} - -PARTITION_ARMING = { - "PartitionID": "1", - "ArmingState": ArmingState.ARMING, -} -PARTITION_DISARMING = { - "PartitionID": "1", - "ArmingState": ArmingState.DISARMING, -} - -PARTITION_TRIGGERED_POLICE = { - "PartitionID": "1", - "ArmingState": ArmingState.ALARMING, -} - -PARTITION_TRIGGERED_FIRE = { - "PartitionID": "1", - "ArmingState": ArmingState.ALARMING_FIRE_SMOKE, -} - -PARTITION_TRIGGERED_CARBON_MONOXIDE = { - "PartitionID": "1", - "ArmingState": ArmingState.ALARMING_CARBON_MONOXIDE, -} - -PARTITION_UNKNOWN = { - "PartitionID": "1", - "ArmingState": "99999", -} - - -PARTITION_INFO_DISARMED = [PARTITION_DISARMED, PARTITION_DISARMED2] -PARTITION_INFO_ARMED_STAY = [PARTITION_ARMED_STAY, PARTITION_ARMED_STAY2] -PARTITION_INFO_ARMED_AWAY = [PARTITION_ARMED_AWAY] -PARTITION_INFO_ARMED_CUSTOM = [PARTITION_ARMED_CUSTOM] -PARTITION_INFO_ARMED_NIGHT = [PARTITION_ARMED_NIGHT] -PARTITION_INFO_ARMING = [PARTITION_ARMING] -PARTITION_INFO_DISARMING = [PARTITION_DISARMING] -PARTITION_INFO_TRIGGERED_POLICE = [PARTITION_TRIGGERED_POLICE] -PARTITION_INFO_TRIGGERED_FIRE = [PARTITION_TRIGGERED_FIRE] -PARTITION_INFO_TRIGGERED_CARBON_MONOXIDE = [PARTITION_TRIGGERED_CARBON_MONOXIDE] -PARTITION_INFO_UNKNOWN = [PARTITION_UNKNOWN] - -PARTITIONS_DISARMED = {"PartitionInfo": PARTITION_INFO_DISARMED} -PARTITIONS_ARMED_STAY = {"PartitionInfo": PARTITION_INFO_ARMED_STAY} -PARTITIONS_ARMED_AWAY = {"PartitionInfo": PARTITION_INFO_ARMED_AWAY} -PARTITIONS_ARMED_CUSTOM = {"PartitionInfo": PARTITION_INFO_ARMED_CUSTOM} -PARTITIONS_ARMED_NIGHT = {"PartitionInfo": PARTITION_INFO_ARMED_NIGHT} -PARTITIONS_ARMING = {"PartitionInfo": PARTITION_INFO_ARMING} -PARTITIONS_DISARMING = {"PartitionInfo": PARTITION_INFO_DISARMING} -PARTITIONS_TRIGGERED_POLICE = {"PartitionInfo": PARTITION_INFO_TRIGGERED_POLICE} -PARTITIONS_TRIGGERED_FIRE = {"PartitionInfo": PARTITION_INFO_TRIGGERED_FIRE} -PARTITIONS_TRIGGERED_CARBON_MONOXIDE = { - "PartitionInfo": PARTITION_INFO_TRIGGERED_CARBON_MONOXIDE -} -PARTITIONS_UNKNOWN = {"PartitionInfo": PARTITION_INFO_UNKNOWN} - -ZONE_NORMAL = { - "ZoneID": "1", - "ZoneDescription": "Security", - "ZoneStatus": ZoneStatus.FAULT, - "ZoneTypeId": ZoneType.SECURITY, - "PartitionId": "1", - "CanBeBypassed": 1, -} -ZONE_2 = { - "ZoneID": "2", - "ZoneDescription": "Fire", - "ZoneStatus": ZoneStatus.LOW_BATTERY, - "ZoneTypeId": ZoneType.FIRE_SMOKE, - "PartitionId": "1", - "CanBeBypassed": 1, -} -ZONE_3 = { - "ZoneID": "3", - "ZoneDescription": "Gas", - "ZoneStatus": ZoneStatus.TAMPER, - "ZoneTypeId": ZoneType.CARBON_MONOXIDE, - "PartitionId": "1", - "CanBeBypassed": 1, -} -ZONE_4 = { - "ZoneID": "4", - "ZoneDescription": "Motion", - "ZoneStatus": ZoneStatus.NORMAL, - "ZoneTypeId": ZoneType.INTERIOR_FOLLOWER, - "PartitionId": "1", - "CanBeBypassed": 1, -} -ZONE_5 = { - "ZoneID": "5", - "ZoneDescription": "Medical", - "ZoneStatus": ZoneStatus.NORMAL, - "ZoneTypeId": ZoneType.PROA7_MEDICAL, - "PartitionId": "1", - "CanBeBypassed": 0, -} -# 99 is an unknown ZoneType -ZONE_6 = { - "ZoneID": "6", - "ZoneDescription": "Unknown", - "ZoneStatus": ZoneStatus.NORMAL, - "ZoneTypeId": 99, - "PartitionId": "1", - "CanBeBypassed": 0, -} - -ZONE_7 = { - "ZoneID": 7, - "ZoneDescription": "Temperature", - "ZoneStatus": ZoneStatus.NORMAL, - "ZoneTypeId": ZoneType.MONITOR, - "PartitionId": "1", - "CanBeBypassed": 0, -} - -# ZoneType security that cannot be bypassed is a Button on the alarm panel -ZONE_8 = { - "ZoneID": 8, - "ZoneDescription": "Button", - "ZoneStatus": ZoneStatus.FAULT, - "ZoneTypeId": ZoneType.SECURITY, - "PartitionId": "1", - "CanBeBypassed": 0, -} - - -ZONE_INFO = [ZONE_NORMAL, ZONE_2, ZONE_3, ZONE_4, ZONE_5, ZONE_6, ZONE_7] -ZONES = {"ZoneInfo": ZONE_INFO} - -METADATA_DISARMED = { - "Partitions": PARTITIONS_DISARMED, - "Zones": ZONES, - "PromptForImportSecuritySettings": False, - "IsInACLoss": False, - "IsCoverTampered": False, - "Bell1SupervisionFailure": False, - "Bell2SupervisionFailure": False, - "IsInLowBattery": False, -} - -METADATA_ARMED_STAY = METADATA_DISARMED.copy() -METADATA_ARMED_STAY["Partitions"] = PARTITIONS_ARMED_STAY - -METADATA_ARMED_AWAY = METADATA_DISARMED.copy() -METADATA_ARMED_AWAY["Partitions"] = PARTITIONS_ARMED_AWAY - -METADATA_ARMED_CUSTOM = METADATA_DISARMED.copy() -METADATA_ARMED_CUSTOM["Partitions"] = PARTITIONS_ARMED_CUSTOM - -METADATA_ARMED_NIGHT = METADATA_DISARMED.copy() -METADATA_ARMED_NIGHT["Partitions"] = PARTITIONS_ARMED_NIGHT - -METADATA_ARMING = METADATA_DISARMED.copy() -METADATA_ARMING["Partitions"] = PARTITIONS_ARMING - -METADATA_DISARMING = METADATA_DISARMED.copy() -METADATA_DISARMING["Partitions"] = PARTITIONS_DISARMING - -METADATA_TRIGGERED_POLICE = METADATA_DISARMED.copy() -METADATA_TRIGGERED_POLICE["Partitions"] = PARTITIONS_TRIGGERED_POLICE - -METADATA_TRIGGERED_FIRE = METADATA_DISARMED.copy() -METADATA_TRIGGERED_FIRE["Partitions"] = PARTITIONS_TRIGGERED_FIRE - -METADATA_TRIGGERED_CARBON_MONOXIDE = METADATA_DISARMED.copy() -METADATA_TRIGGERED_CARBON_MONOXIDE["Partitions"] = PARTITIONS_TRIGGERED_CARBON_MONOXIDE - -METADATA_UNKNOWN = METADATA_DISARMED.copy() -METADATA_UNKNOWN["Partitions"] = PARTITIONS_UNKNOWN - -RESPONSE_DISARMED = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_DISARMED, - "ArmingState": ArmingState.DISARMED, -} -RESPONSE_ARMED_STAY = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_ARMED_STAY, - "ArmingState": ArmingState.ARMED_STAY, -} -RESPONSE_ARMED_AWAY = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_ARMED_AWAY, - "ArmingState": ArmingState.ARMED_AWAY, -} -RESPONSE_ARMED_CUSTOM = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_ARMED_CUSTOM, - "ArmingState": ArmingState.ARMED_CUSTOM_BYPASS, -} -RESPONSE_ARMED_NIGHT = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_ARMED_NIGHT, - "ArmingState": ArmingState.ARMED_STAY_NIGHT, -} -RESPONSE_ARMING = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_ARMING, - "ArmingState": ArmingState.ARMING, -} -RESPONSE_DISARMING = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_DISARMING, - "ArmingState": ArmingState.DISARMING, -} -RESPONSE_TRIGGERED_POLICE = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_TRIGGERED_POLICE, - "ArmingState": ArmingState.ALARMING, -} -RESPONSE_TRIGGERED_FIRE = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_TRIGGERED_FIRE, - "ArmingState": ArmingState.ALARMING_FIRE_SMOKE, -} -RESPONSE_TRIGGERED_CARBON_MONOXIDE = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_TRIGGERED_CARBON_MONOXIDE, - "ArmingState": ArmingState.ALARMING_CARBON_MONOXIDE, -} -RESPONSE_UNKNOWN = { - "ResultCode": 0, - "PanelMetadataAndStatus": METADATA_UNKNOWN, - "ArmingState": ArmingState.DISARMED, -} - -RESPONSE_ARM_SUCCESS = {"ResultCode": ResultCode.ARM_SUCCESS.value} -RESPONSE_ARM_FAILURE = {"ResultCode": ResultCode.COMMAND_FAILED.value} -RESPONSE_DISARM_SUCCESS = {"ResultCode": ResultCode.DISARM_SUCCESS.value} -RESPONSE_DISARM_FAILURE = { - "ResultCode": ResultCode.COMMAND_FAILED.value, - "ResultData": "Command Failed", -} -RESPONSE_USER_CODE_INVALID = { - "ResultCode": ResultCode.USER_CODE_INVALID.value, - "ResultData": "testing user code invalid", -} -RESPONSE_SUCCESS = {"ResultCode": ResultCode.SUCCESS.value} -RESPONSE_ZONE_BYPASS_SUCCESS = { - "ResultCode": ResultCode.SUCCESS.value, - "ResultData": "None", -} -RESPONSE_ZONE_BYPASS_FAILURE = { - "ResultCode": ResultCode.FAILED_TO_BYPASS_ZONE.value, - "ResultData": "None", -} - -USERNAME = "username@me.com" -PASSWORD = "password" -USERCODES = {LOCATION_ID: "7890"} -CONFIG_DATA = { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_USERCODES: USERCODES, -} -CONFIG_DATA_NO_USERCODES = {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - -OPTIONS_DATA = {AUTO_BYPASS: False, CODE_REQUIRED: False} -OPTIONS_DATA_CODE_REQUIRED = {AUTO_BYPASS: False, CODE_REQUIRED: True} - -PARTITION_DETAILS_1 = { - "PartitionID": "1", - "ArmingState": ArmingState.DISARMED.value, - "PartitionName": "Test1", -} - -PARTITION_DETAILS_2 = { - "PartitionID": "2", - "ArmingState": ArmingState.DISARMED.value, - "PartitionName": "Test2", -} - -PARTITION_DETAILS = {"PartitionDetails": [PARTITION_DETAILS_1, PARTITION_DETAILS_2]} -RESPONSE_PARTITION_DETAILS = { - "ResultCode": ResultCode.SUCCESS.value, - "ResultData": "testing partition details", - "PartitionsInfoList": PARTITION_DETAILS, -} - -ZONE_DETAILS_NORMAL = { - "PartitionId": "1", - "Batterylevel": "-1", - "Signalstrength": "-1", - "zoneAdditionalInfo": None, - "ZoneID": "1", - "ZoneStatus": ZoneStatus.NORMAL, - "ZoneTypeId": ZoneType.SECURITY, - "CanBeBypassed": 1, - "ZoneFlags": None, -} - -ZONE_STATUS_INFO = [ZONE_DETAILS_NORMAL] -ZONE_DETAILS = {"ZoneStatusInfoWithPartitionId": ZONE_STATUS_INFO} -ZONE_DETAIL_STATUS = {"Zones": ZONE_DETAILS} - -RESPONSE_GET_ZONE_DETAILS_SUCCESS = { - "ResultCode": 0, - "ResultData": "Success", - "ZoneStatus": ZONE_DETAIL_STATUS, -} - -TOTALCONNECT_REQUEST = ( - "homeassistant.components.totalconnect.TotalConnectClient.request" -) -TOTALCONNECT_GET_CONFIG = ( - "homeassistant.components.totalconnect.TotalConnectClient._get_configuration" -) -TOTALCONNECT_REQUEST_TOKEN = ( - "homeassistant.components.totalconnect.TotalConnectClient._request_token" -) - - -async def setup_platform( - hass: HomeAssistant, platform: Any, code_required: bool = False -) -> MockConfigEntry: - """Set up the TotalConnect platform.""" - # first set up a config entry and add it to hass - if code_required: - mock_entry = MockConfigEntry( - domain=DOMAIN, data=CONFIG_DATA, options=OPTIONS_DATA_CODE_REQUIRED - ) - else: - mock_entry = MockConfigEntry( - domain=DOMAIN, data=CONFIG_DATA, options=OPTIONS_DATA - ) - mock_entry.add_to_hass(hass) - - responses = [ - RESPONSE_SESSION_DETAILS, - RESPONSE_PARTITION_DETAILS, - RESPONSE_GET_ZONE_DETAILS_SUCCESS, - RESPONSE_DISARMED, - RESPONSE_DISARMED, - ] - - with ( - patch("homeassistant.components.totalconnect.PLATFORMS", [platform]), - patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request, - patch(TOTALCONNECT_GET_CONFIG, side_effect=None), - patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), - ): - assert await async_setup_component(hass, DOMAIN, {}) - assert mock_request.call_count == 5 - await hass.async_block_till_done() - - return mock_entry - - -async def init_integration(hass: HomeAssistant) -> MockConfigEntry: - """Set up the TotalConnect integration.""" - # first set up a config entry and add it to hass - mock_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_DATA, options=OPTIONS_DATA) - mock_entry.add_to_hass(hass) - - responses = [ - RESPONSE_SESSION_DETAILS, - RESPONSE_PARTITION_DETAILS, - RESPONSE_GET_ZONE_DETAILS_SUCCESS, - RESPONSE_DISARMED, - RESPONSE_DISARMED, - ] - - with ( - patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request, - patch(TOTALCONNECT_GET_CONFIG, side_effect=None), - patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), - ): - await hass.config_entries.async_setup(mock_entry.entry_id) - assert mock_request.call_count == 5 - await hass.async_block_till_done() - - return mock_entry diff --git a/tests/components/totalconnect/conftest.py b/tests/components/totalconnect/conftest.py new file mode 100644 index 00000000000..803fc052129 --- /dev/null +++ b/tests/components/totalconnect/conftest.py @@ -0,0 +1,249 @@ +"""Configure py.test.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest +from total_connect_client import ArmingState, TotalConnectClient +from total_connect_client.device import TotalConnectDevice +from total_connect_client.location import TotalConnectLocation +from total_connect_client.partition import TotalConnectPartition +from total_connect_client.user import TotalConnectUser +from total_connect_client.zone import TotalConnectZone, ZoneStatus, ZoneType + +from homeassistant.components.totalconnect.const import ( + AUTO_BYPASS, + CODE_REQUIRED, + CONF_USERCODES, + DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import CODE, LOCATION_ID, PASSWORD, USERCODES, USERNAME + +from tests.common import ( + MockConfigEntry, + load_json_array_fixture, + load_json_object_fixture, +) + + +def create_mock_zone( + identifier: int, + partition: str, + description: str, + status: ZoneStatus, + zone_type_id: int, + can_be_bypassed: bool, + battery_level: int, + signal_strength: int, + sensor_serial_number: str | None, + loop_number: int | None, + response_type: str | None, + alarm_report_state: str | None, + supervision_type: str | None, + chime_state: str | None, + device_type: str | None, +) -> AsyncMock: + """Create a mock TotalConnectZone.""" + zone = AsyncMock(spec=TotalConnectZone, autospec=True) + zone.zoneid = identifier + zone.partition = partition + zone.description = description + zone.status = status + zone.zone_type_id = zone_type_id + zone.can_be_bypassed = can_be_bypassed + zone.battery_level = battery_level + zone.signal_strength = signal_strength + zone.sensor_serial_number = sensor_serial_number + zone.loop_number = loop_number + zone.response_type = response_type + zone.alarm_report_state = alarm_report_state + zone.supervision_type = supervision_type + zone.chime_state = chime_state + zone.device_type = device_type + zone.is_type_security.return_value = zone_type_id in ( + ZoneType.SECURITY, + ZoneType.ENTRY_EXIT1, + ZoneType.ENTRY_EXIT2, + ZoneType.PERIMETER, + ZoneType.INTERIOR_FOLLOWER, + ZoneType.TROUBLE_ALARM, + ZoneType.SILENT_24HR, + ZoneType.AUDIBLE_24HR, + ZoneType.INTERIOR_DELAY, + ZoneType.LYRIC_LOCAL_ALARM, + ZoneType.PROA7_GARAGE_MONITOR, + ) + zone.is_type_button.return_value = ( + zone.is_type_security.return_value and not can_be_bypassed + ) or zone_type_id in ( + ZoneType.PROA7_MEDICAL, + ZoneType.AUDIBLE_24HR, + ZoneType.SILENT_24HR, + ZoneType.RF_ARM_STAY, + ZoneType.RF_ARM_AWAY, + ZoneType.RF_DISARM, + ) + return zone + + +def create_mock_zone_from_dict( + zone_data: dict[str, Any], +) -> AsyncMock: + """Create a mock TotalConnectZone from a dictionary.""" + return create_mock_zone( + zone_data["ZoneID"], + zone_data["PartitionId"], + zone_data["ZoneDescription"], + ZoneStatus(zone_data["ZoneStatus"]), + zone_data["ZoneTypeId"], + zone_data["CanBeBypassed"], + zone_data.get("Batterylevel"), + zone_data.get("Signalstrength"), + (zone_data["zoneAdditionalInfo"] or {}).get("SensorSerialNumber"), + (zone_data["zoneAdditionalInfo"] or {}).get("LoopNumber"), + (zone_data["zoneAdditionalInfo"] or {}).get("ResponseType"), + (zone_data["zoneAdditionalInfo"] or {}).get("AlarmReportState"), + (zone_data["zoneAdditionalInfo"] or {}).get("ZoneSupervisionType"), + (zone_data["zoneAdditionalInfo"] or {}).get("ChimeState"), + (zone_data["zoneAdditionalInfo"] or {}).get("DeviceType"), + ) + + +@pytest.fixture +def mock_partition() -> TotalConnectPartition: + """Create a mock TotalConnectPartition.""" + partition = AsyncMock(spec=TotalConnectPartition, autospec=True) + partition.partitionid = 1 + partition.name = "Test1" + partition.is_stay_armed = False + partition.is_fire_armed = False + partition.is_fire_enabled = False + partition.is_common_armed = False + partition.is_common_enabled = False + partition.is_locked = False + partition.is_new_partition = False + partition.is_night_stay_enabled = 0 + partition.exit_delay_timer = 0 + partition.arming_state = ArmingState.DISARMED + return partition + + +@pytest.fixture +def mock_partition_2() -> TotalConnectPartition: + """Create a mock TotalConnectPartition.""" + partition = AsyncMock(spec=TotalConnectPartition, autospec=True) + partition.partitionid = 2 + partition.name = "Test2" + partition.is_stay_armed = False + partition.is_fire_armed = False + partition.is_fire_enabled = False + partition.is_common_armed = False + partition.is_common_enabled = False + partition.is_locked = False + partition.is_new_partition = False + partition.is_night_stay_enabled = 0 + partition.exit_delay_timer = 0 + partition.arming_state = ArmingState.DISARMED + return partition + + +@pytest.fixture +def mock_location( + mock_partition: AsyncMock, mock_partition_2: AsyncMock +) -> TotalConnectLocation: + """Create a mock TotalConnectLocation.""" + location = AsyncMock(spec=TotalConnectLocation, autospec=True) + location.location_id = LOCATION_ID + location.location_name = "Test Location" + location.security_device_id = 7654321 + location.set_usercode.return_value = True + location.partitions = {1: mock_partition, 2: mock_partition_2} + location.devices = { + 7654321: TotalConnectDevice(load_json_object_fixture("device_1.json", DOMAIN)) + } + location.zones = { + z["ZoneID"]: create_mock_zone_from_dict(z) + for z in load_json_array_fixture("zones.json", DOMAIN) + } + location.is_low_battery.return_value = False + location.is_cover_tampered.return_value = False + location.is_ac_loss.return_value = False + location.arming_state = ArmingState.DISARMED + location._module_flags = { + "can_bypass_zones": True, + "can_clear_bypass": True, + "can_set_usercodes": True, + } + location.ac_loss = False + location.low_battery = False + location.auto_bypass_low_battery = False + location.cover_tampered = False + return location + + +@pytest.fixture +def mock_client(mock_location: TotalConnectLocation) -> Generator[TotalConnectClient]: + """Mock a TotalConnectClient for testing.""" + with ( + patch( + "homeassistant.components.totalconnect.config_flow.TotalConnectClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.totalconnect.TotalConnectClient", new=mock_client + ), + ): + client = mock_client.return_value + client.get_number_locations.return_value = 1 + client.locations = {mock_location.location_id: mock_location} + client.usercodes = {mock_location.location_id: CODE} + client.auto_bypass_low_battery = False + client._module_flags = {} + client.retry_delay = 0 + client._invalid_credentials = False + user_mock = AsyncMock(spec=TotalConnectUser, autospec=True) + user_mock._master_user = True + user_mock._user_admin = True + user_mock._config_admin = True + user_mock.security_problem.return_value = False + user_mock._features = { + "can_set_usercodes": True, + "can_bypass_zones": True, + "can_clear_bypass": True, + } + setattr(client, "_user", user_mock) + yield client + + +@pytest.fixture +def code_required() -> bool: + """Return whether a code is required.""" + return False + + +@pytest.fixture +def mock_config_entry(code_required: bool) -> MockConfigEntry: + """Create a mock config entry for testing.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_USERCODES: USERCODES, + }, + options={AUTO_BYPASS: False, CODE_REQUIRED: code_required}, + unique_id=USERNAME, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Mock the setup entry for TotalConnect.""" + with patch( + "homeassistant.components.totalconnect.async_setup_entry", + return_value=True, + ) as mock_setup: + yield mock_setup diff --git a/tests/components/totalconnect/const.py b/tests/components/totalconnect/const.py new file mode 100644 index 00000000000..60024c21011 --- /dev/null +++ b/tests/components/totalconnect/const.py @@ -0,0 +1,8 @@ +"""Constants for the Total Connect tests.""" + +LOCATION_ID = 1234567 +CODE = "7890" + +USERNAME = "username@me.com" +PASSWORD = "password" +USERCODES = {LOCATION_ID: "7890"} diff --git a/tests/components/totalconnect/fixtures/device_1.json b/tests/components/totalconnect/fixtures/device_1.json new file mode 100644 index 00000000000..8ff17092a7d --- /dev/null +++ b/tests/components/totalconnect/fixtures/device_1.json @@ -0,0 +1,12 @@ +{ + "DeviceID": 7654321, + "DeviceName": "test", + "DeviceClassID": 1, + "DeviceSerialNumber": "1234567890AB", + "DeviceFlags": "PromptForUserCode=0,PromptForInstallerCode=0,PromptForImportSecuritySettings=0,AllowUserSlotEditing=0,CalCapable=1,CanBeSentToPanel=1,CanArmNightStay=0,CanSupportMultiPartition=0,PartitionCount=0,MaxPartitionCount=4,OnBoardingSupport=0,PartitionAdded=0,DuplicateUserSyncStatus=0,PanelType=12,PanelVariant=1,BLEDisarmCapable=0,ArmHomeSupported=1,DuplicateUserCodeCheck=1,CanSupportRapid=0,IsKeypadSupported=0,WifiEnrollmentSupported=1,IsConnectedPanel=1,ArmNightInSceneSupported=1,BuiltInCameraSettingsSupported=0,ZWaveThermostatScheduleDisabled=0,MultipleAuthorityLevelSupported=1,VideoOnPanelSupported=1,EnableBLEMode=0,IsPanelWiFiResetSupported=0,IsCompetitorClearBypass=0,IsNotReadyStateSupported=0,isArmStatusWithoutExitDelayNotSupported=0,UserCodeLength=4,UserCodeLengthChanged=0,DoubleDisarmRequired=0,TMSCloudSupported=0,IsAVCEnabled=0", + "SecurityPanelTypeID": 12, + "DeviceSerialText": null, + "DeviceType": null, + "DeviceVariants": null, + "RestrictedPanel": 0 +} diff --git a/tests/components/totalconnect/fixtures/zones.json b/tests/components/totalconnect/fixtures/zones.json new file mode 100644 index 00000000000..2bb237f976b --- /dev/null +++ b/tests/components/totalconnect/fixtures/zones.json @@ -0,0 +1,658 @@ +[ + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "020000", + "LoopNumber": 1, + "ResponseType": "1", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 2, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Security", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-12-11T09:00:13", + "ZoneTypeId": 1 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "030000", + "LoopNumber": 1, + "ResponseType": "4", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 2 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 3, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Fire", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-06-02T15:41:05", + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "040000", + "LoopNumber": 1, + "ResponseType": "4", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 2 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 4, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Gas", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-12-11T09:00:13", + "ZoneTypeId": 14 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "050000", + "LoopNumber": 1, + "ResponseType": "4", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 2 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 5, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Unknown", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-06-02T15:40:59", + "ZoneTypeId": 99 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "060000", + "LoopNumber": 1, + "ResponseType": "1", + "AlarmReportState": 1, + "ZoneSupervisionType": 1, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 6, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Temperature", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 12 + }, + { + "PartitionId": 1, + "Batterylevel": 5, + "Signalstrength": 2, + "zoneAdditionalInfo": { + "SensorSerialNumber": "070000000000000A", + "LoopNumber": 2, + "ResponseType": "53", + "AlarmReportState": 0, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 15 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 7, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Doorbell Other", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 53 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "080000", + "LoopNumber": 1, + "ResponseType": "3", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 8, + "ZoneStatus": 1, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Office Side Door", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 3 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "090000", + "LoopNumber": 1, + "ResponseType": "3", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 9, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Office Back Door", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 3 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "100000", + "LoopNumber": 1, + "ResponseType": "1", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 10, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Master Bedroom Door", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-06-02T15:40:57", + "ZoneTypeId": 1 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "120000", + "LoopNumber": 1, + "ResponseType": "3", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 12, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Dining Room Two Door", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 3 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "130000", + "LoopNumber": 1, + "ResponseType": "3", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 13, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Patio Door", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 3 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "140000", + "LoopNumber": 1, + "ResponseType": "3", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 1 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 14, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Living Room Window", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 3 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "150000", + "LoopNumber": 1, + "ResponseType": "3", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 1 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 15, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Living Room Two Window", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 3 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "160000", + "LoopNumber": 1, + "ResponseType": "9", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 4 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 16, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Apartment SmokeDetector", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:42:29", + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "170000", + "LoopNumber": 1, + "ResponseType": "9", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 4 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 17, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Upstairs Hallway SmokeDetector", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:53:57", + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "180000", + "LoopNumber": 1, + "ResponseType": "9", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 4 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 18, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Downstairs Hallway SmokeDetector", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:47:10", + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "190000", + "LoopNumber": 1, + "ResponseType": "9", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 4 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 19, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Kid Bedroom SmokeDetector", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:49:07", + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "200000", + "LoopNumber": 1, + "ResponseType": "9", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 4 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 20, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Guest Bedroom SmokeDetector", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:50:20", + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "210000", + "LoopNumber": 1, + "ResponseType": "14", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 6 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 21, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Apartment CarbonMonoxideDetecto", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:41:18", + "ZoneTypeId": 14 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "220000", + "LoopNumber": 1, + "ResponseType": "14", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 6 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 22, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Downstairs Hallway CarbonMonoxid", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:45:39", + "ZoneTypeId": 14 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "230000", + "LoopNumber": 1, + "ResponseType": "14", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 6 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 23, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Upstairs Hallway CarbonMonoxideD", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-04-28T09:52:37", + "ZoneTypeId": 14 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": { + "SensorSerialNumber": "240000", + "LoopNumber": 1, + "ResponseType": "9", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 4 + }, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 24, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Master Bedroom SmokeDetector", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": 5, + "Signalstrength": 3, + "zoneAdditionalInfo": { + "SensorSerialNumber": "250000000000000A", + "LoopNumber": 1, + "ResponseType": "23", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 0, + "DeviceType": 15 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 25, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Garage Side Other", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": "2024-12-15T15:14:39", + "ZoneTypeId": 23 + }, + { + "PartitionId": 1, + "Batterylevel": 5, + "Signalstrength": 5, + "zoneAdditionalInfo": { + "SensorSerialNumber": "260000000000000A", + "LoopNumber": 1, + "ResponseType": "1", + "AlarmReportState": 1, + "ZoneSupervisionType": 0, + "ChimeState": 1, + "DeviceType": 0 + }, + "CanBeBypassed": 1, + "ZoneFlags": null, + "ZoneID": 26, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 1, + "ZoneDescription": "Front Door Door", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 1 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": null, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 800, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 0, + "ZoneDescription": "Master Bedroom Keypad", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 50 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": null, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 1995, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 0, + "ZoneDescription": "Zone 995 Fire", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 9 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": null, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 1996, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 0, + "ZoneDescription": "Zone 996 Medical", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 15 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": null, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 1998, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 0, + "ZoneDescription": "Zone 998 Other", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 6 + }, + { + "PartitionId": 1, + "Batterylevel": -1, + "Signalstrength": -1, + "zoneAdditionalInfo": null, + "CanBeBypassed": 0, + "ZoneFlags": null, + "ZoneID": 1999, + "ZoneStatus": 0, + "IsBypassableZone": 0, + "IsSensingZone": 0, + "ZoneDescription": "Zone 999 Police", + "AlarmTriggerTime": null, + "AlarmTriggerTimeLocalized": null, + "ZoneTypeId": 7 + } +] diff --git a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr index 174ab96e8dc..a79fe3832cd 100644 --- a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_attributes[alarm_control_panel.test-entry] +# name: test_entities[alarm_control_panel.test-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -30,11 +30,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': '123456', + 'unique_id': '1234567', 'unit_of_measurement': None, }) # --- -# name: test_attributes[alarm_control_panel.test-state] +# name: test_entities[alarm_control_panel.test-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, @@ -51,7 +51,7 @@ 'state': 'disarmed', }) # --- -# name: test_attributes[alarm_control_panel.test_partition_2-entry] +# name: test_entities[alarm_control_panel.test_partition_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -82,11 +82,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'partition', - 'unique_id': '123456_2', + 'unique_id': '1234567_2', 'unit_of_measurement': None, }) # --- -# name: test_attributes[alarm_control_panel.test_partition_2-state] +# name: test_entities[alarm_control_panel.test_partition_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, diff --git a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr index 75aaddf8572..55702b06acc 100644 --- a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr +++ b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr @@ -1,4 +1,940 @@ # serializer version: 1 +# name: test_entity_registry[binary_sensor.apartment_carbonmonoxidedetecto-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.apartment_carbonmonoxidedetecto', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_21_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_carbonmonoxidedetecto-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Apartment CarbonMonoxideDetecto', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '21', + }), + 'context': , + 'entity_id': 'binary_sensor.apartment_carbonmonoxidedetecto', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_carbonmonoxidedetecto_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.apartment_carbonmonoxidedetecto_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_21_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_carbonmonoxidedetecto_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Apartment CarbonMonoxideDetecto Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '21', + }), + 'context': , + 'entity_id': 'binary_sensor.apartment_carbonmonoxidedetecto_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_carbonmonoxidedetecto_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.apartment_carbonmonoxidedetecto_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_21_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_carbonmonoxidedetecto_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Apartment CarbonMonoxideDetecto Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '21', + }), + 'context': , + 'entity_id': 'binary_sensor.apartment_carbonmonoxidedetecto_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_smokedetector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.apartment_smokedetector', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_16_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_smokedetector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Apartment SmokeDetector', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '16', + }), + 'context': , + 'entity_id': 'binary_sensor.apartment_smokedetector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_smokedetector_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.apartment_smokedetector_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_16_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_smokedetector_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Apartment SmokeDetector Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '16', + }), + 'context': , + 'entity_id': 'binary_sensor.apartment_smokedetector_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_smokedetector_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.apartment_smokedetector_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_16_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.apartment_smokedetector_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Apartment SmokeDetector Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '16', + }), + 'context': , + 'entity_id': 'binary_sensor.apartment_smokedetector_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.dining_room_two_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.dining_room_two_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_12_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.dining_room_two_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Dining Room Two Door', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '12', + }), + 'context': , + 'entity_id': 'binary_sensor.dining_room_two_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.dining_room_two_door_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.dining_room_two_door_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_12_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.dining_room_two_door_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Dining Room Two Door Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '12', + }), + 'context': , + 'entity_id': 'binary_sensor.dining_room_two_door_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.dining_room_two_door_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.dining_room_two_door_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_12_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.dining_room_two_door_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Dining Room Two Door Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '12', + }), + 'context': , + 'entity_id': 'binary_sensor.dining_room_two_door_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.doorbell_other-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.doorbell_other', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_7_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.doorbell_other-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Doorbell Other', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '7', + }), + 'context': , + 'entity_id': 'binary_sensor.doorbell_other', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.doorbell_other_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.doorbell_other_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_7_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.doorbell_other_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Doorbell Other Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '7', + }), + 'context': , + 'entity_id': 'binary_sensor.doorbell_other_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.doorbell_other_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.doorbell_other_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_7_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.doorbell_other_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Doorbell Other Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '7', + }), + 'context': , + 'entity_id': 'binary_sensor.doorbell_other_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_carbonmonoxid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.downstairs_hallway_carbonmonoxid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_22_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_carbonmonoxid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Downstairs Hallway CarbonMonoxid', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '22', + }), + 'context': , + 'entity_id': 'binary_sensor.downstairs_hallway_carbonmonoxid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_carbonmonoxid_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.downstairs_hallway_carbonmonoxid_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_22_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_carbonmonoxid_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Downstairs Hallway CarbonMonoxid Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '22', + }), + 'context': , + 'entity_id': 'binary_sensor.downstairs_hallway_carbonmonoxid_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_carbonmonoxid_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.downstairs_hallway_carbonmonoxid_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_22_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_carbonmonoxid_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Downstairs Hallway CarbonMonoxid Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '22', + }), + 'context': , + 'entity_id': 'binary_sensor.downstairs_hallway_carbonmonoxid_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_smokedetector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.downstairs_hallway_smokedetector', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_18_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_smokedetector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Downstairs Hallway SmokeDetector', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '18', + }), + 'context': , + 'entity_id': 'binary_sensor.downstairs_hallway_smokedetector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_smokedetector_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.downstairs_hallway_smokedetector_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_18_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_smokedetector_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Downstairs Hallway SmokeDetector Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '18', + }), + 'context': , + 'entity_id': 'binary_sensor.downstairs_hallway_smokedetector_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_smokedetector_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.downstairs_hallway_smokedetector_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_18_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.downstairs_hallway_smokedetector_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Downstairs Hallway SmokeDetector Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '18', + }), + 'context': , + 'entity_id': 'binary_sensor.downstairs_hallway_smokedetector_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_entity_registry[binary_sensor.fire-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -30,7 +966,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_2_zone', + 'unique_id': '1234567_3_zone', 'unit_of_measurement': None, }) # --- @@ -39,16 +975,16 @@ 'attributes': ReadOnlyDict({ 'device_class': 'smoke', 'friendly_name': 'Fire', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '2', + 'zone_id': '3', }), 'context': , 'entity_id': 'binary_sensor.fire', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.fire_battery-entry] @@ -82,7 +1018,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_2_low_battery', + 'unique_id': '1234567_3_low_battery', 'unit_of_measurement': None, }) # --- @@ -91,9 +1027,9 @@ 'attributes': ReadOnlyDict({ 'device_class': 'battery', 'friendly_name': 'Fire Battery', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '2', + 'zone_id': '3', }), 'context': , 'entity_id': 'binary_sensor.fire_battery', @@ -134,7 +1070,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_2_tamper', + 'unique_id': '1234567_3_tamper', 'unit_of_measurement': None, }) # --- @@ -143,16 +1079,328 @@ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', 'friendly_name': 'Fire Tamper', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '2', + 'zone_id': '3', }), 'context': , 'entity_id': 'binary_sensor.fire_tamper', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.front_door_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.front_door_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_26_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.front_door_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Front Door Door', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '26', + }), + 'context': , + 'entity_id': 'binary_sensor.front_door_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.front_door_door_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.front_door_door_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_26_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.front_door_door_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Front Door Door Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '26', + }), + 'context': , + 'entity_id': 'binary_sensor.front_door_door_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.front_door_door_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.front_door_door_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_26_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.front_door_door_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Front Door Door Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '26', + }), + 'context': , + 'entity_id': 'binary_sensor.front_door_door_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.garage_side_other-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.garage_side_other', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_25_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.garage_side_other-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Garage Side Other', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '25', + }), + 'context': , + 'entity_id': 'binary_sensor.garage_side_other', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.garage_side_other_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.garage_side_other_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_25_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.garage_side_other_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Garage Side Other Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '25', + }), + 'context': , + 'entity_id': 'binary_sensor.garage_side_other_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.garage_side_other_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.garage_side_other_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_25_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.garage_side_other_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Garage Side Other Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '25', + }), + 'context': , + 'entity_id': 'binary_sensor.garage_side_other_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.gas-entry] @@ -178,7 +1426,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'totalconnect', @@ -186,25 +1434,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_3_zone', + 'unique_id': '1234567_4_zone', 'unit_of_measurement': None, }) # --- # name: test_entity_registry[binary_sensor.gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'gas', + 'device_class': 'smoke', 'friendly_name': 'Gas', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '3', + 'zone_id': '4', }), 'context': , 'entity_id': 'binary_sensor.gas', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.gas_battery-entry] @@ -238,7 +1486,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_3_low_battery', + 'unique_id': '1234567_4_low_battery', 'unit_of_measurement': None, }) # --- @@ -247,16 +1495,16 @@ 'attributes': ReadOnlyDict({ 'device_class': 'battery', 'friendly_name': 'Gas Battery', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '3', + 'zone_id': '4', }), 'context': , 'entity_id': 'binary_sensor.gas_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.gas_tamper-entry] @@ -290,7 +1538,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_3_tamper', + 'unique_id': '1234567_4_tamper', 'unit_of_measurement': None, }) # --- @@ -299,9 +1547,9 @@ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', 'friendly_name': 'Gas Tamper', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '3', + 'zone_id': '4', }), 'context': , 'entity_id': 'binary_sensor.gas_tamper', @@ -311,7 +1559,7 @@ 'state': 'on', }) # --- -# name: test_entity_registry[binary_sensor.medical-entry] +# name: test_entity_registry[binary_sensor.guest_bedroom_smokedetector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -324,7 +1572,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.medical', + 'entity_id': 'binary_sensor.guest_bedroom_smokedetector', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -334,7 +1582,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'totalconnect', @@ -342,80 +1590,28 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_5_zone', + 'unique_id': '1234567_20_zone', 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.medical-state] +# name: test_entity_registry[binary_sensor.guest_bedroom_smokedetector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'safety', - 'friendly_name': 'Medical', - 'location_id': 123456, + 'device_class': 'smoke', + 'friendly_name': 'Guest Bedroom SmokeDetector', + 'location_id': 1234567, 'partition': '1', - 'zone_id': '5', + 'zone_id': '20', }), 'context': , - 'entity_id': 'binary_sensor.medical', + 'entity_id': 'binary_sensor.guest_bedroom_smokedetector', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- -# name: test_entity_registry[binary_sensor.motion-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.motion', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'totalconnect', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '123456_4_zone', - 'unit_of_measurement': None, - }) -# --- -# name: test_entity_registry[binary_sensor.motion-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'Motion', - 'location_id': 123456, - 'partition': '1', - 'zone_id': '4', - }), - 'context': , - 'entity_id': 'binary_sensor.motion', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_entity_registry[binary_sensor.motion_battery-entry] +# name: test_entity_registry[binary_sensor.guest_bedroom_smokedetector_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -428,7 +1624,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.motion_battery', + 'entity_id': 'binary_sensor.guest_bedroom_smokedetector_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -446,28 +1642,28 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_4_low_battery', + 'unique_id': '1234567_20_low_battery', 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.motion_battery-state] +# name: test_entity_registry[binary_sensor.guest_bedroom_smokedetector_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Motion Battery', - 'location_id': 123456, + 'friendly_name': 'Guest Bedroom SmokeDetector Battery', + 'location_id': 1234567, 'partition': '1', - 'zone_id': '4', + 'zone_id': '20', }), 'context': , - 'entity_id': 'binary_sensor.motion_battery', + 'entity_id': 'binary_sensor.guest_bedroom_smokedetector_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- -# name: test_entity_registry[binary_sensor.motion_tamper-entry] +# name: test_entity_registry[binary_sensor.guest_bedroom_smokedetector_tamper-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -480,7 +1676,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.motion_tamper', + 'entity_id': 'binary_sensor.guest_bedroom_smokedetector_tamper', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -498,25 +1694,1429 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_4_tamper', + 'unique_id': '1234567_20_tamper', 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[binary_sensor.motion_tamper-state] +# name: test_entity_registry[binary_sensor.guest_bedroom_smokedetector_tamper-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Motion Tamper', - 'location_id': 123456, + 'friendly_name': 'Guest Bedroom SmokeDetector Tamper', + 'location_id': 1234567, 'partition': '1', - 'zone_id': '4', + 'zone_id': '20', }), 'context': , - 'entity_id': 'binary_sensor.motion_tamper', + 'entity_id': 'binary_sensor.guest_bedroom_smokedetector_tamper', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.kid_bedroom_smokedetector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.kid_bedroom_smokedetector', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_19_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.kid_bedroom_smokedetector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Kid Bedroom SmokeDetector', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '19', + }), + 'context': , + 'entity_id': 'binary_sensor.kid_bedroom_smokedetector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.kid_bedroom_smokedetector_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.kid_bedroom_smokedetector_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_19_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.kid_bedroom_smokedetector_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Kid Bedroom SmokeDetector Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '19', + }), + 'context': , + 'entity_id': 'binary_sensor.kid_bedroom_smokedetector_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.kid_bedroom_smokedetector_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.kid_bedroom_smokedetector_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_19_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.kid_bedroom_smokedetector_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Kid Bedroom SmokeDetector Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '19', + }), + 'context': , + 'entity_id': 'binary_sensor.kid_bedroom_smokedetector_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_two_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.living_room_two_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_15_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_two_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Living Room Two Window', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '15', + }), + 'context': , + 'entity_id': 'binary_sensor.living_room_two_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_two_window_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.living_room_two_window_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_15_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_two_window_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Living Room Two Window Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '15', + }), + 'context': , + 'entity_id': 'binary_sensor.living_room_two_window_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_two_window_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.living_room_two_window_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_15_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_two_window_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Living Room Two Window Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '15', + }), + 'context': , + 'entity_id': 'binary_sensor.living_room_two_window_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.living_room_window', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_14_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Living Room Window', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '14', + }), + 'context': , + 'entity_id': 'binary_sensor.living_room_window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_window_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.living_room_window_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_14_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_window_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Living Room Window Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '14', + }), + 'context': , + 'entity_id': 'binary_sensor.living_room_window_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_window_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.living_room_window_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_14_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.living_room_window_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Living Room Window Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '14', + }), + 'context': , + 'entity_id': 'binary_sensor.living_room_window_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_bedroom_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_10_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Master Bedroom Door', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '10', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_door_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.master_bedroom_door_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_10_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_door_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Master Bedroom Door Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '10', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_door_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_door_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.master_bedroom_door_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_10_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_door_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Master Bedroom Door Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '10', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_door_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_keypad-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_bedroom_keypad', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_800_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_keypad-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Master Bedroom Keypad', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '800', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_keypad', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_keypad_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.master_bedroom_keypad_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_800_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_keypad_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Master Bedroom Keypad Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '800', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_keypad_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_keypad_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.master_bedroom_keypad_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_800_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_keypad_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Master Bedroom Keypad Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '800', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_keypad_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_smokedetector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.master_bedroom_smokedetector', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_24_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_smokedetector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Master Bedroom SmokeDetector', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '24', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_smokedetector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_smokedetector_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.master_bedroom_smokedetector_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_24_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_smokedetector_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Master Bedroom SmokeDetector Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '24', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_smokedetector_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_smokedetector_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.master_bedroom_smokedetector_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_24_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.master_bedroom_smokedetector_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Master Bedroom SmokeDetector Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '24', + }), + 'context': , + 'entity_id': 'binary_sensor.master_bedroom_smokedetector_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.office_back_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.office_back_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_9_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.office_back_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Office Back Door', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '9', + }), + 'context': , + 'entity_id': 'binary_sensor.office_back_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.office_back_door_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.office_back_door_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_9_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.office_back_door_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Office Back Door Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '9', + }), + 'context': , + 'entity_id': 'binary_sensor.office_back_door_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.office_back_door_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.office_back_door_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_9_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.office_back_door_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Office Back Door Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '9', + }), + 'context': , + 'entity_id': 'binary_sensor.office_back_door_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.office_side_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.office_side_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_8_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.office_side_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Office Side Door', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '8', + }), + 'context': , + 'entity_id': 'binary_sensor.office_side_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.office_side_door_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.office_side_door_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_8_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.office_side_door_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Office Side Door Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '8', + }), + 'context': , + 'entity_id': 'binary_sensor.office_side_door_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.office_side_door_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.office_side_door_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_8_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.office_side_door_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Office Side Door Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '8', + }), + 'context': , + 'entity_id': 'binary_sensor.office_side_door_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.patio_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.patio_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_13_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.patio_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Patio Door', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '13', + }), + 'context': , + 'entity_id': 'binary_sensor.patio_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.patio_door_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.patio_door_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_13_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.patio_door_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Patio Door Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '13', + }), + 'context': , + 'entity_id': 'binary_sensor.patio_door_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.patio_door_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.patio_door_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_13_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.patio_door_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Patio Door Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '13', + }), + 'context': , + 'entity_id': 'binary_sensor.patio_door_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.security-entry] @@ -542,7 +3142,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'totalconnect', @@ -550,18 +3150,18 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_1_zone', + 'unique_id': '1234567_2_zone', 'unit_of_measurement': None, }) # --- # name: test_entity_registry[binary_sensor.security-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'door', + 'device_class': 'smoke', 'friendly_name': 'Security', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '1', + 'zone_id': '2', }), 'context': , 'entity_id': 'binary_sensor.security', @@ -602,7 +3202,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_1_low_battery', + 'unique_id': '1234567_2_low_battery', 'unit_of_measurement': None, }) # --- @@ -611,16 +3211,16 @@ 'attributes': ReadOnlyDict({ 'device_class': 'battery', 'friendly_name': 'Security Battery', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '1', + 'zone_id': '2', }), 'context': , 'entity_id': 'binary_sensor.security_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.security_tamper-entry] @@ -654,7 +3254,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_1_tamper', + 'unique_id': '1234567_2_tamper', 'unit_of_measurement': None, }) # --- @@ -663,16 +3263,16 @@ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', 'friendly_name': 'Security Tamper', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '1', + 'zone_id': '2', }), 'context': , 'entity_id': 'binary_sensor.security_tamper', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.temperature-entry] @@ -698,7 +3298,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'totalconnect', @@ -706,25 +3306,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_7_zone', + 'unique_id': '1234567_6_zone', 'unit_of_measurement': None, }) # --- # name: test_entity_registry[binary_sensor.temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'problem', + 'device_class': 'smoke', 'friendly_name': 'Temperature', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': 7, + 'zone_id': '6', }), 'context': , 'entity_id': 'binary_sensor.temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.temperature_battery-entry] @@ -758,7 +3358,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_7_low_battery', + 'unique_id': '1234567_6_low_battery', 'unit_of_measurement': None, }) # --- @@ -767,16 +3367,16 @@ 'attributes': ReadOnlyDict({ 'device_class': 'battery', 'friendly_name': 'Temperature Battery', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': 7, + 'zone_id': '6', }), 'context': , 'entity_id': 'binary_sensor.temperature_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.temperature_tamper-entry] @@ -810,7 +3410,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_7_tamper', + 'unique_id': '1234567_6_tamper', 'unit_of_measurement': None, }) # --- @@ -819,16 +3419,16 @@ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', 'friendly_name': 'Temperature Tamper', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': 7, + 'zone_id': '6', }), 'context': , 'entity_id': 'binary_sensor.temperature_tamper', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.test_battery-entry] @@ -862,7 +3462,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_low_battery', + 'unique_id': '1234567_low_battery', 'unit_of_measurement': None, }) # --- @@ -871,7 +3471,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'battery', 'friendly_name': 'test Battery', - 'location_id': 123456, + 'location_id': 1234567, }), 'context': , 'entity_id': 'binary_sensor.test_battery', @@ -912,7 +3512,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_carbon_monoxide', + 'unique_id': '1234567_carbon_monoxide', 'unit_of_measurement': None, }) # --- @@ -921,7 +3521,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'carbon_monoxide', 'friendly_name': 'test Carbon monoxide', - 'location_id': 123456, + 'location_id': 1234567, }), 'context': , 'entity_id': 'binary_sensor.test_carbon_monoxide', @@ -962,7 +3562,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'police', - 'unique_id': '123456_police', + 'unique_id': '1234567_police', 'unit_of_measurement': None, }) # --- @@ -970,7 +3570,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'test Police emergency', - 'location_id': 123456, + 'location_id': 1234567, }), 'context': , 'entity_id': 'binary_sensor.test_police_emergency', @@ -1011,7 +3611,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_power', + 'unique_id': '1234567_power', 'unit_of_measurement': None, }) # --- @@ -1020,7 +3620,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'power', 'friendly_name': 'test Power', - 'location_id': 123456, + 'location_id': 1234567, }), 'context': , 'entity_id': 'binary_sensor.test_power', @@ -1061,7 +3661,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_smoke', + 'unique_id': '1234567_smoke', 'unit_of_measurement': None, }) # --- @@ -1070,7 +3670,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'smoke', 'friendly_name': 'test Smoke', - 'location_id': 123456, + 'location_id': 1234567, }), 'context': , 'entity_id': 'binary_sensor.test_smoke', @@ -1111,7 +3711,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_tamper', + 'unique_id': '1234567_tamper', 'unit_of_measurement': None, }) # --- @@ -1120,7 +3720,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', 'friendly_name': 'test Tamper', - 'location_id': 123456, + 'location_id': 1234567, }), 'context': , 'entity_id': 'binary_sensor.test_tamper', @@ -1153,7 +3753,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'totalconnect', @@ -1161,25 +3761,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_6_zone', + 'unique_id': '1234567_5_zone', 'unit_of_measurement': None, }) # --- # name: test_entity_registry[binary_sensor.unknown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'door', + 'device_class': 'smoke', 'friendly_name': 'Unknown', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '6', + 'zone_id': '5', }), 'context': , 'entity_id': 'binary_sensor.unknown', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.unknown_battery-entry] @@ -1213,7 +3813,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_6_low_battery', + 'unique_id': '1234567_5_low_battery', 'unit_of_measurement': None, }) # --- @@ -1222,16 +3822,16 @@ 'attributes': ReadOnlyDict({ 'device_class': 'battery', 'friendly_name': 'Unknown Battery', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '6', + 'zone_id': '5', }), 'context': , 'entity_id': 'binary_sensor.unknown_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_entity_registry[binary_sensor.unknown_tamper-entry] @@ -1265,7 +3865,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456_6_tamper', + 'unique_id': '1234567_5_tamper', 'unit_of_measurement': None, }) # --- @@ -1274,15 +3874,951 @@ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', 'friendly_name': 'Unknown Tamper', - 'location_id': 123456, + 'location_id': 1234567, 'partition': '1', - 'zone_id': '6', + 'zone_id': '5', }), 'context': , 'entity_id': 'binary_sensor.unknown_tamper', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_carbonmonoxided-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.upstairs_hallway_carbonmonoxided', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_23_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_carbonmonoxided-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Upstairs Hallway CarbonMonoxideD', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '23', + }), + 'context': , + 'entity_id': 'binary_sensor.upstairs_hallway_carbonmonoxided', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_carbonmonoxided_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.upstairs_hallway_carbonmonoxided_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_23_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_carbonmonoxided_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Upstairs Hallway CarbonMonoxideD Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '23', + }), + 'context': , + 'entity_id': 'binary_sensor.upstairs_hallway_carbonmonoxided_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_carbonmonoxided_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.upstairs_hallway_carbonmonoxided_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_23_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_carbonmonoxided_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Upstairs Hallway CarbonMonoxideD Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '23', + }), + 'context': , + 'entity_id': 'binary_sensor.upstairs_hallway_carbonmonoxided_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_smokedetector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.upstairs_hallway_smokedetector', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_17_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_smokedetector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Upstairs Hallway SmokeDetector', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '17', + }), + 'context': , + 'entity_id': 'binary_sensor.upstairs_hallway_smokedetector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_smokedetector_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.upstairs_hallway_smokedetector_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_17_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_smokedetector_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Upstairs Hallway SmokeDetector Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '17', + }), + 'context': , + 'entity_id': 'binary_sensor.upstairs_hallway_smokedetector_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_smokedetector_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.upstairs_hallway_smokedetector_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_17_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.upstairs_hallway_smokedetector_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Upstairs Hallway SmokeDetector Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '17', + }), + 'context': , + 'entity_id': 'binary_sensor.upstairs_hallway_smokedetector_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_995_fire-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.zone_995_fire', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1995_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_995_fire-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Zone 995 Fire', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1995', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_995_fire', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_995_fire_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_995_fire_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1995_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_995_fire_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Zone 995 Fire Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1995', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_995_fire_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_995_fire_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_995_fire_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1995_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_995_fire_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Zone 995 Fire Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1995', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_995_fire_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_996_medical-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.zone_996_medical', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1996_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_996_medical-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Zone 996 Medical', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1996', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_996_medical', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_996_medical_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_996_medical_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1996_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_996_medical_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Zone 996 Medical Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1996', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_996_medical_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_996_medical_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_996_medical_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1996_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_996_medical_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Zone 996 Medical Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1996', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_996_medical_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_998_other-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.zone_998_other', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1998_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_998_other-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Zone 998 Other', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1998', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_998_other', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_998_other_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_998_other_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1998_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_998_other_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Zone 998 Other Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1998', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_998_other_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_998_other_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_998_other_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1998_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_998_other_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Zone 998 Other Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1998', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_998_other_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_999_police-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.zone_999_police', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1999_zone', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_999_police-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Zone 999 Police', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1999', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_999_police', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_999_police_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_999_police_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1999_low_battery', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_999_police_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Zone 999 Police Battery', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1999', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_999_police_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[binary_sensor.zone_999_police_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.zone_999_police_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567_1999_tamper', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[binary_sensor.zone_999_police_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Zone 999 Police Tamper', + 'location_id': 1234567, + 'partition': '1', + 'zone_id': '1999', + }), + 'context': , + 'entity_id': 'binary_sensor.zone_999_police_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', }) # --- diff --git a/tests/components/totalconnect/snapshots/test_button.ambr b/tests/components/totalconnect/snapshots/test_button.ambr index 4367b035cc8..db90af349cb 100644 --- a/tests/components/totalconnect/snapshots/test_button.ambr +++ b/tests/components/totalconnect/snapshots/test_button.ambr @@ -1,4 +1,100 @@ # serializer version: 1 +# name: test_entity_registry[button.dining_room_two_door_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.dining_room_two_door_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_12_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.dining_room_two_door_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dining Room Two Door Bypass', + }), + 'context': , + 'entity_id': 'button.dining_room_two_door_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_registry[button.doorbell_other_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.doorbell_other_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_7_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.doorbell_other_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Doorbell Other Bypass', + }), + 'context': , + 'entity_id': 'button.doorbell_other_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_entity_registry[button.fire_bypass-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -30,7 +126,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', - 'unique_id': '123456_2_bypass', + 'unique_id': '1234567_3_bypass', 'unit_of_measurement': None, }) # --- @@ -47,6 +143,102 @@ 'state': 'unknown', }) # --- +# name: test_entity_registry[button.front_door_door_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.front_door_door_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_26_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.front_door_door_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Front Door Door Bypass', + }), + 'context': , + 'entity_id': 'button.front_door_door_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_registry[button.garage_side_other_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.garage_side_other_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_25_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.garage_side_other_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Side Other Bypass', + }), + 'context': , + 'entity_id': 'button.garage_side_other_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_entity_registry[button.gas_bypass-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -78,7 +270,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', - 'unique_id': '123456_3_bypass', + 'unique_id': '1234567_4_bypass', 'unit_of_measurement': None, }) # --- @@ -95,7 +287,7 @@ 'state': 'unknown', }) # --- -# name: test_entity_registry[button.motion_bypass-entry] +# name: test_entity_registry[button.living_room_two_window_bypass-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -108,7 +300,7 @@ 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.motion_bypass', + 'entity_id': 'button.living_room_two_window_bypass', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -126,17 +318,257 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', - 'unique_id': '123456_4_bypass', + 'unique_id': '1234567_15_bypass', 'unit_of_measurement': None, }) # --- -# name: test_entity_registry[button.motion_bypass-state] +# name: test_entity_registry[button.living_room_two_window_bypass-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Motion Bypass', + 'friendly_name': 'Living Room Two Window Bypass', }), 'context': , - 'entity_id': 'button.motion_bypass', + 'entity_id': 'button.living_room_two_window_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_registry[button.living_room_window_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.living_room_window_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_14_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.living_room_window_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Living Room Window Bypass', + }), + 'context': , + 'entity_id': 'button.living_room_window_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_registry[button.master_bedroom_door_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.master_bedroom_door_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_10_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.master_bedroom_door_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Master Bedroom Door Bypass', + }), + 'context': , + 'entity_id': 'button.master_bedroom_door_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_registry[button.office_back_door_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.office_back_door_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_9_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.office_back_door_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Office Back Door Bypass', + }), + 'context': , + 'entity_id': 'button.office_back_door_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_registry[button.office_side_door_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.office_side_door_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_8_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.office_side_door_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Office Side Door Bypass', + }), + 'context': , + 'entity_id': 'button.office_side_door_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity_registry[button.patio_door_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.patio_door_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_13_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.patio_door_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Patio Door Bypass', + }), + 'context': , + 'entity_id': 'button.patio_door_bypass', 'last_changed': , 'last_reported': , 'last_updated': , @@ -174,7 +606,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', - 'unique_id': '123456_1_bypass', + 'unique_id': '1234567_2_bypass', 'unit_of_measurement': None, }) # --- @@ -191,6 +623,54 @@ 'state': 'unknown', }) # --- +# name: test_entity_registry[button.temperature_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.temperature_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_6_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.temperature_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Temperature Bypass', + }), + 'context': , + 'entity_id': 'button.temperature_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_entity_registry[button.test_bypass_all-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -222,7 +702,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass_all', - 'unique_id': '123456_bypass_all', + 'unique_id': '1234567_bypass_all', 'unit_of_measurement': None, }) # --- @@ -270,7 +750,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clear_bypass', - 'unique_id': '123456_clear_bypass', + 'unique_id': '1234567_clear_bypass', 'unit_of_measurement': None, }) # --- @@ -287,3 +767,51 @@ 'state': 'unknown', }) # --- +# name: test_entity_registry[button.unknown_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.unknown_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'totalconnect', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bypass', + 'unique_id': '1234567_5_bypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[button.unknown_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Unknown Bypass', + }), + 'context': , + 'entity_id': 'button.unknown_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/totalconnect/snapshots/test_diagnostics.ambr b/tests/components/totalconnect/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..026afca0920 --- /dev/null +++ b/tests/components/totalconnect/snapshots/test_diagnostics.ambr @@ -0,0 +1,619 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'client': dict({ + 'auto_bypass_low_battery': False, + 'invalid_credentials': False, + 'module_flags': dict({ + }), + 'retry_delay': 0, + }), + 'locations': list([ + dict({ + 'ac_loss': False, + 'arming_state': dict({ + '__type': "", + 'repr': '', + }), + 'auto_bypass_low_battery': False, + 'cover_tampered': False, + 'devices': list([ + dict({ + 'class_id': 1, + 'device_id': 7654321, + 'flags': dict({ + 'AllowUserSlotEditing': '0', + 'ArmHomeSupported': '1', + 'ArmNightInSceneSupported': '1', + 'BLEDisarmCapable': '0', + 'BuiltInCameraSettingsSupported': '0', + 'CalCapable': '1', + 'CanArmNightStay': '0', + 'CanBeSentToPanel': '1', + 'CanSupportMultiPartition': '0', + 'CanSupportRapid': '0', + 'DoubleDisarmRequired': '0', + 'DuplicateUserCodeCheck': '1', + 'DuplicateUserSyncStatus': '0', + 'EnableBLEMode': '0', + 'IsAVCEnabled': '0', + 'IsCompetitorClearBypass': '0', + 'IsConnectedPanel': '1', + 'IsKeypadSupported': '0', + 'IsNotReadyStateSupported': '0', + 'IsPanelWiFiResetSupported': '0', + 'MaxPartitionCount': '4', + 'MultipleAuthorityLevelSupported': '1', + 'OnBoardingSupport': '0', + 'PanelType': '12', + 'PanelVariant': '1', + 'PartitionAdded': '0', + 'PartitionCount': '0', + 'PromptForImportSecuritySettings': '0', + 'PromptForInstallerCode': '0', + 'PromptForUserCode': '0', + 'TMSCloudSupported': '0', + 'UserCodeLength': '4', + 'UserCodeLengthChanged': '0', + 'VideoOnPanelSupported': '1', + 'WifiEnrollmentSupported': '1', + 'ZWaveThermostatScheduleDisabled': '0', + 'isArmStatusWithoutExitDelayNotSupported': '0', + }), + 'name': 'test', + 'security_panel_type_id': 12, + 'serial_number': '**REDACTED**', + 'serial_text': None, + }), + ]), + 'location_id': 1234567, + 'low_battery': False, + 'module_flags': dict({ + 'can_bypass_zones': True, + 'can_clear_bypass': True, + 'can_set_usercodes': True, + }), + 'name': 'Test Location', + 'partitions': list([ + dict({ + 'arming_state': dict({ + '__type': "", + 'repr': '', + }), + 'exit_delay_timer': 0, + 'is_common_enabled': False, + 'is_fire_enabled': False, + 'is_locked': False, + 'is_new_partition': False, + 'is_night_stay_enabled': 0, + 'is_stay_armed': False, + 'name': 'Test1', + 'partition_id': 1, + }), + dict({ + 'arming_state': dict({ + '__type': "", + 'repr': '', + }), + 'exit_delay_timer': 0, + 'is_common_enabled': False, + 'is_fire_enabled': False, + 'is_locked': False, + 'is_new_partition': False, + 'is_night_stay_enabled': 0, + 'is_stay_armed': False, + 'name': 'Test2', + 'partition_id': 2, + }), + ]), + 'security_device_id': 7654321, + 'zones': list([ + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Security', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '1', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 2, + 'zone_type_id': 1, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 0, + 'description': 'Fire', + 'device_type': 2, + 'loop_number': 1, + 'partition': 1, + 'response_type': '4', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 3, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 0, + 'description': 'Gas', + 'device_type': 2, + 'loop_number': 1, + 'partition': 1, + 'response_type': '4', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 4, + 'zone_type_id': 14, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 0, + 'description': 'Unknown', + 'device_type': 2, + 'loop_number': 1, + 'partition': 1, + 'response_type': '4', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 5, + 'zone_type_id': 99, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Temperature', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '1', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 1, + 'zone_id': 6, + 'zone_type_id': 12, + }), + dict({ + 'alarm_report_state': 0, + 'battery_level': 5, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Doorbell Other', + 'device_type': 15, + 'loop_number': 2, + 'partition': 1, + 'response_type': '53', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': 2, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 7, + 'zone_type_id': 53, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Office Side Door', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '3', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 1, + 'supervision_type': 0, + 'zone_id': 8, + 'zone_type_id': 3, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Office Back Door', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '3', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 9, + 'zone_type_id': 3, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Master Bedroom Door', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '1', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 10, + 'zone_type_id': 1, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Dining Room Two Door', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '3', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 12, + 'zone_type_id': 3, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Patio Door', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '3', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 13, + 'zone_type_id': 3, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Living Room Window', + 'device_type': 1, + 'loop_number': 1, + 'partition': 1, + 'response_type': '3', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 14, + 'zone_type_id': 3, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Living Room Two Window', + 'device_type': 1, + 'loop_number': 1, + 'partition': 1, + 'response_type': '3', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 15, + 'zone_type_id': 3, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Apartment SmokeDetector', + 'device_type': 4, + 'loop_number': 1, + 'partition': 1, + 'response_type': '9', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 16, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Upstairs Hallway SmokeDetector', + 'device_type': 4, + 'loop_number': 1, + 'partition': 1, + 'response_type': '9', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 17, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Downstairs Hallway SmokeDetector', + 'device_type': 4, + 'loop_number': 1, + 'partition': 1, + 'response_type': '9', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 18, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Kid Bedroom SmokeDetector', + 'device_type': 4, + 'loop_number': 1, + 'partition': 1, + 'response_type': '9', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 19, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Guest Bedroom SmokeDetector', + 'device_type': 4, + 'loop_number': 1, + 'partition': 1, + 'response_type': '9', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 20, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Apartment CarbonMonoxideDetecto', + 'device_type': 6, + 'loop_number': 1, + 'partition': 1, + 'response_type': '14', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 21, + 'zone_type_id': 14, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Downstairs Hallway CarbonMonoxid', + 'device_type': 6, + 'loop_number': 1, + 'partition': 1, + 'response_type': '14', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 22, + 'zone_type_id': 14, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Upstairs Hallway CarbonMonoxideD', + 'device_type': 6, + 'loop_number': 1, + 'partition': 1, + 'response_type': '14', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 23, + 'zone_type_id': 14, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': 0, + 'description': 'Master Bedroom SmokeDetector', + 'device_type': 4, + 'loop_number': 1, + 'partition': 1, + 'response_type': '9', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': -1, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 24, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': 5, + 'can_be_bypassed': 1, + 'chime_state': 0, + 'description': 'Garage Side Other', + 'device_type': 15, + 'loop_number': 1, + 'partition': 1, + 'response_type': '23', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': 3, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 25, + 'zone_type_id': 23, + }), + dict({ + 'alarm_report_state': 1, + 'battery_level': 5, + 'can_be_bypassed': 1, + 'chime_state': 1, + 'description': 'Front Door Door', + 'device_type': 0, + 'loop_number': 1, + 'partition': 1, + 'response_type': '1', + 'sensor_serial_number': '**REDACTED**', + 'signal_strength': 5, + 'status': 0, + 'supervision_type': 0, + 'zone_id': 26, + 'zone_type_id': 1, + }), + dict({ + 'alarm_report_state': None, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': None, + 'description': 'Master Bedroom Keypad', + 'device_type': None, + 'loop_number': None, + 'partition': 1, + 'response_type': None, + 'sensor_serial_number': None, + 'signal_strength': -1, + 'status': 0, + 'supervision_type': None, + 'zone_id': 800, + 'zone_type_id': 50, + }), + dict({ + 'alarm_report_state': None, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': None, + 'description': 'Zone 995 Fire', + 'device_type': None, + 'loop_number': None, + 'partition': 1, + 'response_type': None, + 'sensor_serial_number': None, + 'signal_strength': -1, + 'status': 0, + 'supervision_type': None, + 'zone_id': 1995, + 'zone_type_id': 9, + }), + dict({ + 'alarm_report_state': None, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': None, + 'description': 'Zone 996 Medical', + 'device_type': None, + 'loop_number': None, + 'partition': 1, + 'response_type': None, + 'sensor_serial_number': None, + 'signal_strength': -1, + 'status': 0, + 'supervision_type': None, + 'zone_id': 1996, + 'zone_type_id': 15, + }), + dict({ + 'alarm_report_state': None, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': None, + 'description': 'Zone 998 Other', + 'device_type': None, + 'loop_number': None, + 'partition': 1, + 'response_type': None, + 'sensor_serial_number': None, + 'signal_strength': -1, + 'status': 0, + 'supervision_type': None, + 'zone_id': 1998, + 'zone_type_id': 6, + }), + dict({ + 'alarm_report_state': None, + 'battery_level': -1, + 'can_be_bypassed': 0, + 'chime_state': None, + 'description': 'Zone 999 Police', + 'device_type': None, + 'loop_number': None, + 'partition': 1, + 'response_type': None, + 'sensor_serial_number': None, + 'signal_strength': -1, + 'status': 0, + 'supervision_type': None, + 'zone_id': 1999, + 'zone_type_id': 7, + }), + ]), + }), + ]), + 'user': dict({ + 'config_admin': True, + 'features': dict({ + 'can_bypass_zones': True, + 'can_clear_bypass': True, + 'can_set_usercodes': True, + }), + 'master': True, + 'security_problem': False, + 'user_admin': True, + }), + }) +# --- diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 6f7d8163362..040cdf5d9ed 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -1,19 +1,16 @@ """Tests for the TotalConnect alarm control panel device.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from total_connect_client.exceptions import ( - AuthenticationError, - ServiceUnavailable, - TotalConnectError, -) +from total_connect_client import ArmingState, ArmType +from total_connect_client.exceptions import BadResultCodeError, UsercodeInvalid from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_DOMAIN, + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, AlarmControlPanelState, ) from homeassistant.components.totalconnect.alarm_control_panel import ( @@ -21,593 +18,375 @@ from homeassistant.components.totalconnect.alarm_control_panel import ( SERVICE_ALARM_ARM_HOME_INSTANT, ) from homeassistant.components.totalconnect.const import DOMAIN -from homeassistant.components.totalconnect.coordinator import SCAN_INTERVAL -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, - STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_component import async_update_entity -from .common import ( - LOCATION_ID, - RESPONSE_ARM_FAILURE, - RESPONSE_ARM_SUCCESS, - RESPONSE_ARMED_AWAY, - RESPONSE_ARMED_CUSTOM, - RESPONSE_ARMED_NIGHT, - RESPONSE_ARMED_STAY, - RESPONSE_ARMING, - RESPONSE_DISARM_FAILURE, - RESPONSE_DISARM_SUCCESS, - RESPONSE_DISARMED, - RESPONSE_DISARMING, - RESPONSE_SUCCESS, - RESPONSE_UNKNOWN, - RESPONSE_USER_CODE_INVALID, - TOTALCONNECT_REQUEST, - USERCODES, - setup_platform, -) +from . import setup_integration +from .const import CODE -from tests.common import async_fire_time_changed, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform ENTITY_ID = "alarm_control_panel.test" ENTITY_ID_2 = "alarm_control_panel.test_partition_2" -CODE = "-1" DATA = {ATTR_ENTITY_ID: ENTITY_ID} DELAY = timedelta(seconds=10) +ARMING_HELPER = "homeassistant.components.totalconnect.alarm_control_panel.ArmingHelper" -async def test_attributes( - hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion + +async def test_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test the alarm control panel attributes are correct.""" - entry = await setup_platform(hass, ALARM_DOMAIN) with patch( - "homeassistant.components.totalconnect.TotalConnectClient.request", - return_value=RESPONSE_DISARMED, - ) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - mock_request.assert_called_once() + "homeassistant.components.totalconnect.PLATFORMS", + [Platform.ALARM_CONTROL_PANEL], + ): + await setup_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) - assert mock_request.call_count == 1 + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_arm_home_success( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize("code_required", [False, True]) +@pytest.mark.parametrize( + ("service", "arm_type"), + [ + (SERVICE_ALARM_ARM_HOME, ArmType.STAY), + (SERVICE_ALARM_ARM_NIGHT, ArmType.STAY_NIGHT), + (SERVICE_ALARM_ARM_AWAY, ArmType.AWAY), + ], +) +async def test_arming( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + arm_type: ArmType, ) -> None: - """Test arm home method success.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert hass.states.get(ENTITY_ID_2).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 + """Test arming method success.""" + await setup_integration(hass, mock_config_entry) - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True - ) - assert mock_request.call_count == 2 + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_HOME - # second partition should not be armed - assert hass.states.get(ENTITY_ID_2).state == AlarmControlPanelState.DISARMED + mock_partition.arming_state = ArmingState.ARMING + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id, ATTR_CODE: CODE}, + blocking=True, + ) + assert mock_partition.arm.call_args[1] == {"arm_type": arm_type, "usercode": ""} + + assert hass.states.get(entity_id).state == AlarmControlPanelState.ARMING -async def test_arm_home_failure(hass: HomeAssistant) -> None: - """Test arm home method failure.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Failed to arm home test" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 2 - - # config entry usercode is invalid - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Usercode is invalid, did not arm home" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - # should have started a re-auth flow - assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 - assert mock_request.call_count == 3 - - -async def test_arm_home_instant_success( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize("code_required", [True]) +@pytest.mark.parametrize( + ("service", "arm_type"), + [ + (SERVICE_ALARM_ARM_HOME, ArmType.STAY), + (SERVICE_ALARM_ARM_NIGHT, ArmType.STAY_NIGHT), + (SERVICE_ALARM_ARM_AWAY, ArmType.AWAY), + ], +) +async def test_arming_invalid_usercode( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_location: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + arm_type: ArmType, ) -> None: - """Test arm home instant method success.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert hass.states.get(ENTITY_ID_2).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 + """Test arming method with invalid usercode.""" + await setup_integration(hass, mock_config_entry) + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED + assert mock_location.get_panel_meta_data.call_count == 1 + + mock_partition.arming_state = ArmingState.ARMING + + with pytest.raises(ServiceValidationError, match="Incorrect code entered"): await hass.services.async_call( - DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True + ALARM_CONTROL_PANEL_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id, ATTR_CODE: "invalid_code"}, + blocking=True, ) - assert mock_request.call_count == 2 - - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_HOME + assert mock_partition.arm.call_count == 0 + assert mock_location.get_panel_meta_data.call_count == 1 -async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: - """Test arm home instant method failure.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Failed to arm home instant test" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 2 - - # usercode is invalid - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True - ) - await hass.async_block_till_done() - assert str(err.value) == "Usercode is invalid, did not arm home instant" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - # should have started a re-auth flow - assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 - assert mock_request.call_count == 3 - - -async def test_arm_away_instant_success( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize("code_required", [False, True]) +async def test_disarming( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: - """Test arm home instant method success.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert hass.states.get(ENTITY_ID_2).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 + """Test disarming method success.""" + await setup_integration(hass, mock_config_entry) - await hass.services.async_call( - DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True - ) - assert mock_request.call_count == 2 + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY + mock_partition.arming_state = ArmingState.ARMING + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_DISARM, + {ATTR_ENTITY_ID: entity_id, ATTR_CODE: CODE}, + blocking=True, + ) + assert mock_partition.disarm.call_args[1] == {"usercode": ""} + + assert hass.states.get(entity_id).state == AlarmControlPanelState.ARMING -async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: - """Test arm home instant method failure.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Failed to arm away instant test" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 2 - - # usercode is invalid - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Usercode is invalid, did not arm away instant" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - # should have started a re-auth flow - assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 - assert mock_request.call_count == 3 - - -async def test_arm_away_success( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize("code_required", [True]) +async def test_disarming_invalid_usercode( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_location: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: - """Test arm away method success.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 + """Test disarming method with invalid usercode.""" + await setup_integration(hass, mock_config_entry) + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED + assert mock_location.get_panel_meta_data.call_count == 1 + + mock_partition.arming_state = ArmingState.ARMING + + with pytest.raises(ServiceValidationError, match="Incorrect code entered"): await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_DISARM, + {ATTR_ENTITY_ID: entity_id, ATTR_CODE: "invalid_code"}, + blocking=True, ) - assert mock_request.call_count == 2 - - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY + assert mock_partition.disarm.call_count == 0 + assert mock_location.get_panel_meta_data.call_count == 1 -async def test_arm_away_failure(hass: HomeAssistant) -> None: - """Test arm away method failure.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Failed to arm away test" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 2 - - # usercode is invalid - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Usercode is invalid, did not arm away" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - # should have started a re-auth flow - assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 - assert mock_request.call_count == 3 - - -async def test_disarm_success( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize( + ("service", "arm_type"), + [ + (SERVICE_ALARM_ARM_HOME_INSTANT, ArmType.STAY_INSTANT), + (SERVICE_ALARM_ARM_AWAY_INSTANT, ArmType.AWAY_INSTANT), + ], +) +async def test_instant_arming( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + arm_type: ArmType, ) -> None: - """Test disarm method success.""" - responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY - assert mock_request.call_count == 1 + """Test instant arming method success.""" + await setup_integration(hass, mock_config_entry) - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True - ) - assert mock_request.call_count == 2 + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED + mock_partition.arming_state = ArmingState.ARMING + + await hass.services.async_call( + DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert mock_partition.arm.call_args[1] == {"arm_type": arm_type, "usercode": ""} + + assert hass.states.get(entity_id).state == AlarmControlPanelState.ARMING -async def test_disarm_failure(hass: HomeAssistant) -> None: - """Test disarm method failure.""" - responses = [ - RESPONSE_ARMED_AWAY, - RESPONSE_DISARM_FAILURE, - RESPONSE_USER_CODE_INVALID, - ] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY - assert mock_request.call_count == 1 - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Failed to disarm test" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY - assert mock_request.call_count == 2 - - # usercode is invalid - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Usercode is invalid, did not disarm" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY - # should have started a re-auth flow - assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 - assert mock_request.call_count == 3 - - -async def test_disarm_code_required( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize( + ("exception", "suffix", "flows"), + [(UsercodeInvalid, "invalid_code", 1), (BadResultCodeError, "failed", 0)], +) +@pytest.mark.parametrize( + ("service", "prefix"), + [ + (SERVICE_ALARM_ARM_HOME, "arm_home"), + (SERVICE_ALARM_ARM_NIGHT, "arm_night"), + (SERVICE_ALARM_ARM_AWAY, "arm_away"), + ], +) +async def test_arming_exceptions( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_location: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + prefix: str, + exception: Exception, + suffix: str, + flows: int, ) -> None: - """Test disarm with code.""" - responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED] - await setup_platform(hass, ALARM_DOMAIN, code_required=True) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY - assert mock_request.call_count == 1 + """Test arming method exceptions.""" + await setup_integration(hass, mock_config_entry) - # runtime user entered code is bad - DATA_WITH_CODE = DATA.copy() - DATA_WITH_CODE["code"] = "666" - with pytest.raises(ServiceValidationError, match="Incorrect code entered"): - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA_WITH_CODE, blocking=True - ) - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY - # code check means the call to total_connect never happens - assert mock_request.call_count == 1 + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED + assert mock_location.get_panel_meta_data.call_count == 1 - # runtime user entered code that is in config - DATA_WITH_CODE["code"] = USERCODES[LOCATION_ID] + mock_partition.arm.side_effect = exception + + mock_partition.arming_state = ArmingState.ARMING + + with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA_WITH_CODE, blocking=True + ALARM_CONTROL_PANEL_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id, ATTR_CODE: CODE}, + blocking=True, ) - await hass.async_block_till_done() - assert mock_request.call_count == 2 + assert mock_partition.arm.call_count == 1 - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED + assert exc.value.translation_key == f"{prefix}_{suffix}" + + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED + assert mock_location.get_panel_meta_data.call_count == 1 + + assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == flows -async def test_arm_night_success( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize( + ("exception", "suffix", "flows"), + [(UsercodeInvalid, "invalid_code", 1), (BadResultCodeError, "failed", 0)], +) +@pytest.mark.parametrize( + ("service", "prefix"), + [ + (SERVICE_ALARM_ARM_HOME_INSTANT, "arm_home_instant"), + (SERVICE_ALARM_ARM_AWAY_INSTANT, "arm_away_instant"), + ], +) +async def test_instant_arming_exceptions( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_location: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + prefix: str, + exception: Exception, + suffix: str, + flows: int, ) -> None: - """Test arm night method success.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_NIGHT] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 + """Test arming method exceptions.""" + await setup_integration(hass, mock_config_entry) + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED + assert mock_location.get_panel_meta_data.call_count == 1 + + mock_partition.arm.side_effect = exception + + mock_partition.arming_state = ArmingState.ARMING + + with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True + DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) - assert mock_request.call_count == 2 + assert mock_partition.arm.call_count == 1 - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_NIGHT + assert exc.value.translation_key == f"{prefix}_{suffix}" + + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED + assert mock_location.get_panel_meta_data.call_count == 1 + + assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == flows -async def test_arm_night_failure(hass: HomeAssistant) -> None: - """Test arm night method failure.""" - responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 - - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Failed to arm night test" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 2 - - # usercode is invalid - with pytest.raises(HomeAssistantError) as err: - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True - ) - await hass.async_block_till_done() - assert f"{err.value}" == "Usercode is invalid, did not arm night" - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - # should have started a re-auth flow - assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 - assert mock_request.call_count == 3 - - -async def test_arming(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: - """Test arming.""" - responses = [RESPONSE_DISARMED, RESPONSE_SUCCESS, RESPONSE_ARMING] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 - - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True - ) - assert mock_request.call_count == 2 - - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMING - - -async def test_disarming(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: - """Test disarming.""" - responses = [RESPONSE_ARMED_AWAY, RESPONSE_SUCCESS, RESPONSE_DISARMING] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.ARMED_AWAY - assert mock_request.call_count == 1 - - await hass.services.async_call( - ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True - ) - assert mock_request.call_count == 2 - - freezer.tick(DELAY) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert mock_request.call_count == 3 - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMING - - -async def test_armed_custom(hass: HomeAssistant) -> None: - """Test armed custom.""" - responses = [RESPONSE_ARMED_CUSTOM] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert ( - hass.states.get(ENTITY_ID).state - == AlarmControlPanelState.ARMED_CUSTOM_BYPASS - ) - assert mock_request.call_count == 1 - - -async def test_unknown(hass: HomeAssistant) -> None: - """Test unknown arm status.""" - responses = [RESPONSE_UNKNOWN] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - assert mock_request.call_count == 1 - - -async def test_other_update_failures( - hass: HomeAssistant, freezer: FrozenDateTimeFactory +@pytest.mark.parametrize( + ("arming_state", "state"), + [ + (ArmingState.DISARMED, AlarmControlPanelState.DISARMED), + (ArmingState.DISARMED_BYPASS, AlarmControlPanelState.DISARMED), + (ArmingState.DISARMED_ZONE_FAULTED, AlarmControlPanelState.DISARMED), + (ArmingState.ARMED_STAY_NIGHT, AlarmControlPanelState.ARMED_NIGHT), + (ArmingState.ARMED_STAY_NIGHT_BYPASS_PROA7, AlarmControlPanelState.ARMED_NIGHT), + ( + ArmingState.ARMED_STAY_NIGHT_INSTANT_PROA7, + AlarmControlPanelState.ARMED_NIGHT, + ), + ( + ArmingState.ARMED_STAY_NIGHT_INSTANT_BYPASS_PROA7, + AlarmControlPanelState.ARMED_NIGHT, + ), + (ArmingState.ARMED_STAY, AlarmControlPanelState.ARMED_HOME), + (ArmingState.ARMED_STAY_PROA7, AlarmControlPanelState.ARMED_HOME), + (ArmingState.ARMED_STAY_BYPASS, AlarmControlPanelState.ARMED_HOME), + (ArmingState.ARMED_STAY_BYPASS_PROA7, AlarmControlPanelState.ARMED_HOME), + (ArmingState.ARMED_STAY_INSTANT, AlarmControlPanelState.ARMED_HOME), + (ArmingState.ARMED_STAY_INSTANT_PROA7, AlarmControlPanelState.ARMED_HOME), + (ArmingState.ARMED_STAY_INSTANT_BYPASS, AlarmControlPanelState.ARMED_HOME), + ( + ArmingState.ARMED_STAY_INSTANT_BYPASS_PROA7, + AlarmControlPanelState.ARMED_HOME, + ), + (ArmingState.ARMED_STAY_OTHER, AlarmControlPanelState.ARMED_HOME), + (ArmingState.ARMED_AWAY, AlarmControlPanelState.ARMED_AWAY), + (ArmingState.ARMED_AWAY_BYPASS, AlarmControlPanelState.ARMED_AWAY), + (ArmingState.ARMED_AWAY_INSTANT, AlarmControlPanelState.ARMED_AWAY), + (ArmingState.ARMED_AWAY_INSTANT_BYPASS, AlarmControlPanelState.ARMED_AWAY), + (ArmingState.ARMED_CUSTOM_BYPASS, AlarmControlPanelState.ARMED_CUSTOM_BYPASS), + (ArmingState.ARMING, AlarmControlPanelState.ARMING), + (ArmingState.DISARMING, AlarmControlPanelState.DISARMING), + (ArmingState.ALARMING, AlarmControlPanelState.TRIGGERED), + (ArmingState.ALARMING_FIRE_SMOKE, AlarmControlPanelState.TRIGGERED), + (ArmingState.ALARMING_CARBON_MONOXIDE, AlarmControlPanelState.TRIGGERED), + (ArmingState.ALARMING_CARBON_MONOXIDE_PROA7, AlarmControlPanelState.TRIGGERED), + ], +) +async def test_arming_state( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_partition: AsyncMock, + mock_location: AsyncMock, + mock_config_entry: MockConfigEntry, + arming_state: ArmingState, + state: AlarmControlPanelState, + freezer: FrozenDateTimeFactory, ) -> None: - """Test other failures seen during updates.""" - responses = [ - RESPONSE_DISARMED, - ServiceUnavailable, - RESPONSE_DISARMED, - TotalConnectError, - RESPONSE_DISARMED, - ValueError, - ] - await setup_platform(hass, ALARM_DOMAIN) - with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - # first things work as planned - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 1 + """Test arming state transitions.""" + await setup_integration(hass, mock_config_entry) - # then an error: ServiceUnavailable --> UpdateFailed - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - assert mock_request.call_count == 2 + entity_id = "alarm_control_panel.test" + assert hass.states.get(entity_id).state == AlarmControlPanelState.DISARMED - # works again - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 3 + mock_partition.arming_state = arming_state - # then an error: TotalConnectError --> UpdateFailed - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - assert mock_request.call_count == 4 + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) - # works again - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - assert hass.states.get(ENTITY_ID).state == AlarmControlPanelState.DISARMED - assert mock_request.call_count == 5 - - # unknown TotalConnect status via ValueError - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE - assert mock_request.call_count == 6 - - -async def test_authentication_error(hass: HomeAssistant) -> None: - """Test other failures seen during updates.""" - entry = await setup_platform(hass, ALARM_DOMAIN) - - with patch(TOTALCONNECT_REQUEST, side_effect=AuthenticationError): - await async_update_entity(hass, ENTITY_ID) - await hass.async_block_till_done() - - assert entry.state is ConfigEntryState.LOADED - - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - - flow = flows[0] - assert flow.get("step_id") == "reauth_confirm" - assert flow.get("handler") == DOMAIN - - assert "context" in flow - assert flow["context"].get("source") == SOURCE_REAUTH - assert flow["context"].get("entry_id") == entry.entry_id + assert hass.states.get(entity_id).state == state diff --git a/tests/components/totalconnect/test_binary_sensor.py b/tests/components/totalconnect/test_binary_sensor.py index 8910487ea58..3083dd8c629 100644 --- a/tests/components/totalconnect/test_binary_sensor.py +++ b/tests/components/totalconnect/test_binary_sensor.py @@ -1,91 +1,29 @@ """Tests for the TotalConnect binary sensor.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from syrupy.assertion import SnapshotAssertion -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR, - BinarySensorDeviceClass, -) -from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .common import RESPONSE_DISARMED, ZONE_NORMAL, setup_platform +from . import setup_integration -from tests.common import snapshot_platform - -ZONE_ENTITY_ID = "binary_sensor.security" -ZONE_LOW_BATTERY_ID = "binary_sensor.security_battery" -ZONE_TAMPER_ID = "binary_sensor.security_tamper" -PANEL_BATTERY_ID = "binary_sensor.test_battery" -PANEL_TAMPER_ID = "binary_sensor.test_tamper" -PANEL_POWER_ID = "binary_sensor.test_power" +from tests.common import MockConfigEntry, snapshot_platform async def test_entity_registry( - hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: - """Test the binary sensor is registered in entity registry.""" - entry = await setup_platform(hass, BINARY_SENSOR) - - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) - - -async def test_state_and_attributes(hass: HomeAssistant) -> None: - """Test the binary sensor attributes are correct.""" - + """Test the alarm control panel attributes are correct.""" with patch( - "homeassistant.components.totalconnect.TotalConnectClient.request", - return_value=RESPONSE_DISARMED, + "homeassistant.components.totalconnect.PLATFORMS", [Platform.BINARY_SENSOR] ): - await setup_platform(hass, BINARY_SENSOR) + await setup_integration(hass, mock_config_entry) - state = hass.states.get(ZONE_ENTITY_ID) - assert state.state == STATE_ON - assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == ZONE_NORMAL["ZoneDescription"] - ) - assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR - - state = hass.states.get(f"{ZONE_ENTITY_ID}_battery") - assert state.state == STATE_OFF - state = hass.states.get(f"{ZONE_ENTITY_ID}_tamper") - assert state.state == STATE_OFF - - # Zone 2 is fire with low battery - state = hass.states.get("binary_sensor.fire") - assert state.state == STATE_OFF - assert state.attributes.get("device_class") == BinarySensorDeviceClass.SMOKE - state = hass.states.get("binary_sensor.fire_battery") - assert state.state == STATE_ON - state = hass.states.get("binary_sensor.fire_tamper") - assert state.state == STATE_OFF - - # Zone 3 is gas with tamper - state = hass.states.get("binary_sensor.gas") - assert state.state == STATE_OFF - assert state.attributes.get("device_class") == BinarySensorDeviceClass.GAS - state = hass.states.get("binary_sensor.gas_battery") - assert state.state == STATE_OFF - state = hass.states.get("binary_sensor.gas_tamper") - assert state.state == STATE_ON - - # Zone 6 is unknown type, assume it is a security (door) sensor - state = hass.states.get("binary_sensor.unknown") - assert state.state == STATE_OFF - assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR - state = hass.states.get("binary_sensor.unknown_battery") - assert state.state == STATE_OFF - state = hass.states.get("binary_sensor.unknown_tamper") - assert state.state == STATE_OFF - - # Zone 7 is temperature - state = hass.states.get("binary_sensor.temperature") - assert state.state == STATE_OFF - assert state.attributes.get("device_class") == BinarySensorDeviceClass.PROBLEM - state = hass.states.get("binary_sensor.temperature_battery") - assert state.state == STATE_OFF - state = hass.states.get("binary_sensor.temperature_tamper") - assert state.state == STATE_OFF + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/totalconnect/test_button.py b/tests/components/totalconnect/test_button.py index 092b058e693..9492d815152 100644 --- a/tests/components/totalconnect/test_button.py +++ b/tests/components/totalconnect/test_button.py @@ -1,84 +1,64 @@ """Tests for the TotalConnect buttons.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -import pytest from syrupy.assertion import SnapshotAssertion -from total_connect_client.exceptions import FailedToBypassZone -from homeassistant.components.button import DOMAIN as BUTTON, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .common import setup_platform +from . import setup_integration -from tests.common import snapshot_platform - -ZONE_BYPASS_ID = "button.security_bypass" -PANEL_CLEAR_ID = "button.test_clear_bypass" -PANEL_BYPASS_ID = "button.test_bypass_all" +from tests.common import MockConfigEntry, snapshot_platform async def test_entity_registry( - hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test the button is registered in entity registry.""" - entry = await setup_platform(hass, BUTTON) + with patch("homeassistant.components.totalconnect.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - ("entity_id", "tcc_request"), - [ - (ZONE_BYPASS_ID, "total_connect_client.zone.TotalConnectZone.bypass"), - ( - PANEL_BYPASS_ID, - "total_connect_client.location.TotalConnectLocation.zone_bypass_all", - ), - ], -) async def test_bypass_button( - hass: HomeAssistant, entity_id: str, tcc_request: str + hass: HomeAssistant, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_location: AsyncMock, ) -> None: """Test pushing a bypass button.""" - responses = [FailedToBypassZone, None] - await setup_platform(hass, BUTTON) - with patch(tcc_request, side_effect=responses) as mock_request: - # try to bypass, but fails - with pytest.raises(FailedToBypassZone): - await hass.services.async_call( - domain=BUTTON, - service=SERVICE_PRESS, - service_data={ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - assert mock_request.call_count == 1 - - # try to bypass, works this time - await hass.services.async_call( - domain=BUTTON, - service=SERVICE_PRESS, - service_data={ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - assert mock_request.call_count == 2 - - -async def test_clear_button(hass: HomeAssistant) -> None: - """Test pushing the clear bypass button.""" - data = {ATTR_ENTITY_ID: PANEL_CLEAR_ID} - await setup_platform(hass, BUTTON) - TOTALCONNECT_REQUEST = ( - "total_connect_client.location.TotalConnectLocation.clear_bypass" + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.security_bypass"}, + blocking=True, ) - with patch(TOTALCONNECT_REQUEST) as mock_request: - await hass.services.async_call( - domain=BUTTON, - service=SERVICE_PRESS, - service_data=data, - blocking=True, - ) - assert mock_request.call_count == 1 + assert mock_location.zones[2].bypass.call_count == 1 + + +async def test_clear_button( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_location: AsyncMock, +) -> None: + """Test pushing the clear bypass button.""" + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.test_clear_bypass"}, + blocking=True, + ) + + assert mock_location.clear_bypass.call_count == 1 diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index b7ac42c84b5..dbbff265129 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for the TotalConnect config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from total_connect_client.exceptions import AuthenticationError @@ -11,217 +11,235 @@ from homeassistant.components.totalconnect.const import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_PASSWORD +from homeassistant.const import CONF_LOCATION, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .common import ( - CONFIG_DATA, - CONFIG_DATA_NO_USERCODES, - RESPONSE_DISARMED, - RESPONSE_GET_ZONE_DETAILS_SUCCESS, - RESPONSE_PARTITION_DETAILS, - RESPONSE_SESSION_DETAILS, - RESPONSE_SUCCESS, - RESPONSE_USER_CODE_INVALID, - TOTALCONNECT_GET_CONFIG, - TOTALCONNECT_REQUEST, - TOTALCONNECT_REQUEST_TOKEN, - USERNAME, - init_integration, -) +from . import setup_integration +from .const import LOCATION_ID, PASSWORD, USERNAME from tests.common import MockConfigEntry -async def test_user(hass: HomeAssistant) -> None: +async def test_full_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_client: AsyncMock +) -> None: """Test user step.""" - # user starts with no data entered, so show the user form result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=None, + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) -async def test_user_show_locations(hass: HomeAssistant) -> None: - """Test user locations form.""" - # user/pass provided, so check if valid then ask for usercodes on locations form - responses = [ - RESPONSE_SESSION_DETAILS, - RESPONSE_PARTITION_DETAILS, - RESPONSE_GET_ZONE_DETAILS_SUCCESS, - RESPONSE_DISARMED, - RESPONSE_USER_CODE_INVALID, - RESPONSE_SUCCESS, - ] + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "locations" - with ( - patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request, - patch(TOTALCONNECT_GET_CONFIG, side_effect=None), - patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), - patch( - "homeassistant.components.totalconnect.async_setup_entry", return_value=True - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=CONFIG_DATA_NO_USERCODES, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERCODES: "7890"} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_PASSWORD: PASSWORD, + CONF_USERNAME: USERNAME, + CONF_USERCODES: {LOCATION_ID: "7890"}, + } + assert result["title"] == "Total Connect" + assert result["options"] == {} + assert result["result"].unique_id == USERNAME + + +async def test_login_errors( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_client: AsyncMock +) -> None: + """Test login errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.totalconnect.config_flow.TotalConnectClient", + ) as client: + client.side_effect = AuthenticationError() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - # first it should show the locations form - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "locations" - # client should have sent four requests for init - assert mock_request.call_count == 4 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} - # user enters an invalid usercode - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_USERCODES: "bad"}, - ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "locations" - # client should have sent 5th request to validate usercode - assert mock_request.call_count == 5 + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) - # user enters a valid usercode - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - user_input={CONF_USERCODES: "7890"}, - ) - assert result3["type"] is FlowResultType.CREATE_ENTRY - # client should have sent another request to validate usercode - assert mock_request.call_count == 6 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "locations" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERCODES: "7890"} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_abort_if_already_setup(hass: HomeAssistant) -> None: +async def test_usercode_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: AsyncMock, + mock_location: AsyncMock, +) -> None: + """Test user step with usercode errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "locations" + + mock_location.set_usercode.return_value = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERCODES: "7890"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "locations" + assert result["errors"] == {CONF_LOCATION: "usercode"} + + mock_location.set_usercode.return_value = True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERCODES: "7890"} + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_no_locations( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: AsyncMock, + mock_location: AsyncMock, +) -> None: + """Test no locations found.""" + + mock_client.get_number_locations.return_value = 0 + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_locations" + + +async def test_abort_if_already_setup( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test abort if the account is already setup.""" - MockConfigEntry( - domain=DOMAIN, - data=CONFIG_DATA, - unique_id=USERNAME, - ).add_to_hass(hass) + mock_config_entry.add_to_hass(hass) - # Should fail, same USERNAME (flow) - with patch("homeassistant.components.totalconnect.config_flow.TotalConnectClient"): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=CONFIG_DATA, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_login_failed(hass: HomeAssistant) -> None: - """Test when we have errors during login.""" - with patch( - "homeassistant.components.totalconnect.config_flow.TotalConnectClient" - ) as client_mock: - client_mock.side_effect = AuthenticationError() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=CONFIG_DATA, - ) +async def test_reauth( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test login errors.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": "invalid_auth"} - - -async def test_reauth(hass: HomeAssistant) -> None: - """Test reauth.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG_DATA, - unique_id=USERNAME, - ) - entry.add_to_hass(hass) - - result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with ( - patch( - "homeassistant.components.totalconnect.config_flow.TotalConnectClient" - ) as client_mock, - patch( - "homeassistant.components.totalconnect.async_setup_entry", return_value=True - ), - ): - # first test with an invalid password - client_mock.side_effect = AuthenticationError() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: "abc"} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.data[CONF_PASSWORD] == "abc" + + +async def test_reauth_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test login errors.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.totalconnect.config_flow.TotalConnectClient", + ) as client: + client.side_effect = AuthenticationError() result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PASSWORD: "password"} + result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": "invalid_auth"} - # now test with the password valid - client_mock.side_effect = None + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PASSWORD: "password"} - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: PASSWORD} + ) - assert len(hass.config_entries.async_entries()) == 1 + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" -async def test_no_locations(hass: HomeAssistant) -> None: - """Test with no user locations.""" - responses = [ - RESPONSE_SESSION_DETAILS, - RESPONSE_PARTITION_DETAILS, - RESPONSE_GET_ZONE_DETAILS_SUCCESS, - RESPONSE_DISARMED, - ] - - with ( - patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request, - patch(TOTALCONNECT_GET_CONFIG, side_effect=None), - patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), - patch( - "homeassistant.components.totalconnect.async_setup_entry", return_value=True - ), - patch( - "homeassistant.components.totalconnect.TotalConnectClient.get_number_locations", - return_value=0, - ), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=CONFIG_DATA_NO_USERCODES, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "no_locations" - await hass.async_block_till_done() - - assert mock_request.call_count == 1 - - -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test config flow options.""" - config_entry = await init_integration(hass) - result = await hass.config_entries.options.async_init(config_entry.entry_id) + await setup_integration(hass, mock_config_entry) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" @@ -231,8 +249,4 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert config_entry.options == {AUTO_BYPASS: True, CODE_REQUIRED: False} - await hass.async_block_till_done() - - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() + assert mock_config_entry.options == {AUTO_BYPASS: True, CODE_REQUIRED: False} diff --git a/tests/components/totalconnect/test_diagnostics.py b/tests/components/totalconnect/test_diagnostics.py index 2ad05c60936..7422ee36143 100644 --- a/tests/components/totalconnect/test_diagnostics.py +++ b/tests/components/totalconnect/test_diagnostics.py @@ -1,36 +1,29 @@ """Test TotalConnect diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + from homeassistant.core import HomeAssistant -from .common import LOCATION_ID, init_integration +from . import setup_integration +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + mock_client: AsyncMock, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: """Test config entry diagnostics.""" - entry = await init_integration(hass) + await setup_integration(hass, mock_config_entry) - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - - client = result["client"] - assert client["invalid_credentials"] is False - - user = result["user"] - assert user["master"] is False - - location = result["locations"][0] - assert location["location_id"] == LOCATION_ID - - device = location["devices"][0] - assert device["serial_number"] == REDACTED - - partition = location["partitions"][0] - assert partition["name"] == "Test1" - - zone = location["zones"][0] - assert zone["zone_id"] == "1" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py index ba533e19798..b19f585965f 100644 --- a/tests/components/totalconnect/test_init.py +++ b/tests/components/totalconnect/test_init.py @@ -4,29 +4,23 @@ from unittest.mock import patch from total_connect_client.exceptions import AuthenticationError -from homeassistant.components.totalconnect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from .common import CONFIG_DATA +from . import setup_integration from tests.common import MockConfigEntry -async def test_reauth_started(hass: HomeAssistant) -> None: +async def test_reauth_start( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: """Test that reauth is started when we have login errors.""" - mock_entry = MockConfigEntry( - domain=DOMAIN, - data=CONFIG_DATA, - ) - mock_entry.add_to_hass(hass) - with patch( "homeassistant.components.totalconnect.TotalConnectClient", ) as mock_client: mock_client.side_effect = AuthenticationError() - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await setup_integration(hass, mock_config_entry) - assert mock_entry.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/tts/test_entity.py b/tests/components/tts/test_entity.py index 8648ca95e93..308d3bb0fca 100644 --- a/tests/components/tts/test_entity.py +++ b/tests/components/tts/test_entity.py @@ -175,3 +175,31 @@ def test_streaming_supported() -> None: sync_non_streaming_entity = SyncNonStreamingEntity() assert sync_non_streaming_entity.async_supports_streaming_input() is False + + +async def test_internal_get_tts_audio_writes_state( + hass: HomeAssistant, + mock_tts_entity: MockTTSEntity, +) -> None: + """Test that only async_internal_get_tts_audio updates and writes the state.""" + + entity_id = f"{tts.DOMAIN}.{TEST_DOMAIN}" + + config_entry = await mock_config_entry_setup(hass, mock_tts_entity) + assert config_entry.state is ConfigEntryState.LOADED + state1 = hass.states.get(entity_id) + assert state1 is not None + + # State should *not* change with external method + await mock_tts_entity.async_get_tts_audio("test message", hass.config.language, {}) + state2 = hass.states.get(entity_id) + assert state2 is not None + assert state1.state == state2.state + + # State *should* change with internal method + await mock_tts_entity.async_internal_get_tts_audio( + "test message", hass.config.language, {} + ) + state3 = hass.states.get(entity_id) + assert state3 is not None + assert state1.state != state3.state diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index db42da5de0e..be155aae182 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -2032,3 +2032,34 @@ async def test_tts_cache() -> None: assert await consume_mid_data_task == b"012" with pytest.raises(ValueError): assert await consume_pre_data_loaded_task == b"012" + + +async def test_async_internal_get_tts_audio_called( + hass: HomeAssistant, + mock_tts_entity: MockTTSEntity, + hass_client: ClientSessionGenerator, +) -> None: + """Test that non-streaming entity has its async_internal_get_tts_audio method called.""" + + await mock_config_entry_setup(hass, mock_tts_entity) + + # Non-streaming + assert mock_tts_entity.async_supports_streaming_input() is False + + with patch( + "homeassistant.components.tts.entity.TextToSpeechEntity.async_internal_get_tts_audio" + ) as internal_get_tts_audio: + media_source_id = tts.generate_media_source_id( + hass, + "test message", + "tts.test", + "en_US", + cache=None, + ) + + url = await get_media_source_url(hass, media_source_id) + client = await hass_client() + await client.get(url) + + # async_internal_get_tts_audio is called + internal_get_tts_audio.assert_called_once_with("test message", "en_US", {}) diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 1def19a06bd..1fdc28bcb9f 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -8,470 +8,231 @@ from unittest.mock import patch from tuya_sharing import CustomerDevice from homeassistant.components.tuya import DeviceListener, ManagerCompat -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -DEVICE_MOCKS = { - "cl_3r8gc33pnqsxfe1g": [ - # https://github.com/tuya/tuya-home-assistant/issues/754 - Platform.COVER, - Platform.SENSOR, - Platform.SWITCH, - ], - "cl_cpbo62rn": [ - # https://github.com/orgs/home-assistant/discussions/539 - Platform.COVER, - Platform.SELECT, - ], - "cl_ebt12ypvexnixvtf": [ - # https://github.com/tuya/tuya-home-assistant/issues/754 - Platform.COVER, - ], - "cl_qqdxfdht": [ - # https://github.com/orgs/home-assistant/discussions/539 - Platform.COVER, - ], - "cl_zah67ekd": [ - # https://github.com/home-assistant/core/issues/71242 - Platform.COVER, - Platform.SELECT, - ], - "clkg_nhyj64w2": [ - # https://github.com/home-assistant/core/issues/136055 - Platform.COVER, - Platform.LIGHT, - ], - "co2bj_yrr3eiyiacm31ski": [ - # https://github.com/home-assistant/core/issues/133173 - Platform.BINARY_SENSOR, - Platform.NUMBER, - Platform.SELECT, - Platform.SENSOR, - Platform.SIREN, - ], - "cs_ka2wfrdoogpvgzfi": [ - # https://github.com/home-assistant/core/issues/119865 - Platform.BINARY_SENSOR, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - ], - "cs_qhxmvae667uap4zh": [ - # https://github.com/home-assistant/core/issues/141278 - Platform.FAN, - Platform.HUMIDIFIER, - ], - "cs_vmxuxszzjwp5smli": [ - # https://github.com/home-assistant/core/issues/119865 - Platform.FAN, - Platform.HUMIDIFIER, - ], - "cs_zibqa9dutqyaxym2": [ - Platform.BINARY_SENSOR, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - ], - "cwjwq_agwu93lr": [ - # https://github.com/orgs/home-assistant/discussions/79 - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - ], - "cwwsq_wfkzyy0evslzsmoi": [ - # https://github.com/home-assistant/core/issues/144745 - Platform.NUMBER, - Platform.SENSOR, - ], - "cwysj_z3rpyvznfcch99aa": [ - # https://github.com/home-assistant/core/pull/146599 - Platform.SENSOR, - Platform.SWITCH, - ], - "cz_0g1fmqh6d5io7lcn": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.SWITCH, - ], - "cz_2jxesipczks0kdct": [ - # https://github.com/home-assistant/core/issues/147149 - Platform.SENSOR, - Platform.SWITCH, - ], - "cz_cuhokdii7ojyw8k2": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.SWITCH, - ], - "cz_dntgh2ngvshfxpsz": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.SWITCH, - ], - "cz_hj0a5c7ckzzexu8l": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.SENSOR, - Platform.SWITCH, - ], - "cz_t0a4hwsf8anfsadp": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.SELECT, - Platform.SWITCH, - ], - "dc_l3bpgg8ibsagon4x": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_8szt7whdvwpmxglk": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_8y0aquaa8v6tho8w": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_baf9tt9lb8t5uc7z": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_d4g0fbsoaal841o6": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_djnozmdyqyriow8z": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_ekwolitfjhxn55js": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_fuupmcr2mb1odkja": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_hp6orhaqm6as3jnv": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_hpc8ddyfv85haxa7": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_iayz2jmtlipjnxj7": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_idnfq7xbx8qewyoa": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_ilddqqih3tucdk68": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_j1bgp31cffutizub": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_lmnt3uyltk1xffrt": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_mki13ie507rlry4r": [ - # https://github.com/home-assistant/core/pull/126242 - Platform.LIGHT, - ], - "dj_nbumqpv8vz61enji": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_nlxvjzy1hoeiqsg6": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_oe0cpnjg": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_riwp3k79": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_tmsloaroqavbucgn": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_ufq2xwuzd4nb0qdr": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_vqwcnabamzrc2kab": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_xokdfs6kh5ednakk": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_zakhnlpdiu0ycdxn": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_zav1pa32pyxray78": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dj_zputiamzanuk6yky": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - ], - "dlq_0tnvg2xaisqdadcf": [ - # https://github.com/home-assistant/core/issues/102769 - Platform.SENSOR, - Platform.SWITCH, - ], - "dlq_kxdr6su0c55p7bbo": [ - # https://github.com/home-assistant/core/issues/143499 - Platform.SENSOR, - ], - "fs_g0ewlb1vmwqljzji": [ - # https://github.com/home-assistant/core/issues/141231 - Platform.FAN, - Platform.LIGHT, - Platform.SELECT, - ], - "fs_ibytpo6fpnugft1c": [ - # https://github.com/home-assistant/core/issues/135541 - Platform.FAN, - ], - "gyd_lgekqfxdabipm3tn": [ - # https://github.com/home-assistant/core/issues/133173 - Platform.LIGHT, - ], - "hps_2aaelwxk": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.BINARY_SENSOR, - Platform.NUMBER, - ], - "kg_gbm9ata1zrzaez4a": [ - # https://github.com/home-assistant/core/issues/148347 - Platform.SWITCH, - ], - "kj_CAjWAxBUZt7QZHfz": [ - # https://github.com/home-assistant/core/issues/146023 - Platform.FAN, - Platform.SWITCH, - ], - "kj_yrzylxax1qspdgpp": [ - # https://github.com/orgs/home-assistant/discussions/61 - Platform.FAN, - Platform.SELECT, - Platform.SWITCH, - ], - "ks_j9fa8ahzac8uvlfl": [ - # https://github.com/orgs/home-assistant/discussions/329 - Platform.FAN, - Platform.LIGHT, - Platform.SWITCH, - ], - "kt_5wnlzekkstwcdsvm": [ - # https://github.com/home-assistant/core/pull/148646 - Platform.CLIMATE, - ], - "mal_gyitctrjj1kefxp2": [ - # Alarm Host support - Platform.ALARM_CONTROL_PANEL, - Platform.NUMBER, - Platform.SWITCH, - ], - "mcs_7jIGJAymiH8OsFFb": [ - # https://github.com/home-assistant/core/issues/108301 - Platform.BINARY_SENSOR, - Platform.SENSOR, - ], - "mzj_qavcakohisj5adyh": [ - # https://github.com/home-assistant/core/issues/141278 - Platform.NUMBER, - Platform.SENSOR, - Platform.SWITCH, - ], - "pc_t2afic7i3v1bwhfp": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.SWITCH, - ], - "pc_trjopo1vdlt9q1tg": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.SWITCH, - ], - "pir_3amxzozho9xp4mkh": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.BINARY_SENSOR, - Platform.SENSOR, - ], - "pir_fcdjzz3s": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.BINARY_SENSOR, - Platform.SENSOR, - ], - "pir_wqz93nrdomectyoz": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.BINARY_SENSOR, - Platform.SENSOR, - ], - "qccdz_7bvgooyjhiua1yyq": [ - # https://github.com/home-assistant/core/issues/136207 - Platform.SWITCH, - ], - "qxj_fsea1lat3vuktbt6": [ - # https://github.com/orgs/home-assistant/discussions/318 - Platform.SENSOR, - ], - "qxj_is2indt9nlth6esa": [ - # https://github.com/home-assistant/core/issues/136472 - Platform.SENSOR, - ], - "rqbj_4iqe2hsfyd86kwwc": [ - # https://github.com/orgs/home-assistant/discussions/100 - Platform.BINARY_SENSOR, - Platform.SENSOR, - ], - "sd_lr33znaodtyarrrz": [ - # https://github.com/home-assistant/core/issues/141278 - Platform.BUTTON, - Platform.NUMBER, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ], - "sfkzq_o6dagifntoafakst": [ - # https://github.com/home-assistant/core/issues/148116 - Platform.SWITCH, - ], - "sgbj_ulv4nnue7gqp0rjk": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.NUMBER, - Platform.SELECT, - Platform.SIREN, - ], - "sp_drezasavompxpcgm": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.CAMERA, - Platform.LIGHT, - Platform.SELECT, - Platform.SWITCH, - ], - "sp_rjKXWRohlvOTyLBu": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.CAMERA, - Platform.LIGHT, - Platform.SELECT, - Platform.SWITCH, - ], - "sp_sdd5f5f2dl5wydjf": [ - # https://github.com/home-assistant/core/issues/144087 - Platform.CAMERA, - Platform.NUMBER, - Platform.SENSOR, - Platform.SELECT, - Platform.SIREN, - Platform.SWITCH, - ], - "tdq_1aegphq4yfd50e6b": [ - # https://github.com/home-assistant/core/issues/143209 - Platform.SELECT, - Platform.SWITCH, - ], - "tdq_9htyiowaf5rtdhrv": [ - # https://github.com/home-assistant/core/issues/143209 - Platform.SELECT, - Platform.SWITCH, - ], - "tdq_cq1p0nt0a4rixnex": [ - # https://github.com/home-assistant/core/issues/146845 - Platform.SELECT, - Platform.SWITCH, - ], - "tdq_nockvv2k39vbrxxk": [ - # https://github.com/home-assistant/core/issues/145849 - Platform.SWITCH, - ], - "tdq_pu8uhxhwcp3tgoz7": [ - # https://github.com/home-assistant/core/issues/141278 - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - ], - "tdq_uoa3mayicscacseb": [ - # https://github.com/home-assistant/core/issues/128911 - # SDK information is empty - ], - "tyndj_pyakuuoc": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.LIGHT, - Platform.SENSOR, - Platform.SWITCH, - ], - "wfcon_b25mh8sxawsgndck": [ - # https://github.com/home-assistant/core/issues/149704 - ], - "wk_aqoouq7x": [ - # https://github.com/home-assistant/core/issues/146263 - Platform.CLIMATE, - Platform.SWITCH, - ], - "wg2_nwxr8qcu4seltoro": [ - # https://github.com/orgs/home-assistant/discussions/430 - Platform.BINARY_SENSOR, - ], - "wk_fi6dne5tu4t1nm6j": [ - # https://github.com/orgs/home-assistant/discussions/243 - Platform.CLIMATE, - Platform.NUMBER, - Platform.SENSOR, - Platform.SWITCH, - ], - "wsdcg_g2y6z3p3ja2qhyav": [ - # https://github.com/home-assistant/core/issues/102769 - Platform.SENSOR, - ], - "wxkg_l8yaz4um5b3pwyvf": [ - # https://github.com/home-assistant/core/issues/93975 - Platform.EVENT, - Platform.SENSOR, - ], - "ydkt_jevroj5aguwdbs2e": [ - # https://github.com/orgs/home-assistant/discussions/288 - # unsupported device - no platforms - ], - "ywbj_gf9dejhmzffgdyfj": [ - # https://github.com/home-assistant/core/issues/149704 - Platform.BINARY_SENSOR, - Platform.SENSOR, - ], - "ywcgq_h8lvyoahr6s6aybf": [ - # https://github.com/home-assistant/core/issues/145932 - Platform.NUMBER, - Platform.SENSOR, - ], - "ywcgq_wtzwyhkev3b4ubns": [ - # https://github.com/home-assistant/core/issues/103818 - Platform.NUMBER, - Platform.SENSOR, - ], - "zndb_ze8faryrxr0glqnn": [ - # https://github.com/home-assistant/core/issues/138372 - Platform.SENSOR, - ], - "zwjcy_myd45weu": [ - # https://github.com/orgs/home-assistant/discussions/482 - Platform.SENSOR, - ], -} +DEVICE_MOCKS = [ + "bzyd_45idzfufidgee7ir", # https://github.com/orgs/home-assistant/discussions/717 + "bzyd_ssimhf6r8kgwepfb", # https://github.com/orgs/home-assistant/discussions/718 + "ckmkzq_1yyqfw4djv9eii3q", # https://github.com/home-assistant/core/issues/150856 + "cl_3r8gc33pnqsxfe1g", # https://github.com/tuya/tuya-home-assistant/issues/754 + "cl_669wsr2w4cvinbh4", # https://github.com/home-assistant/core/issues/150856 + "cl_cpbo62rn", # https://github.com/orgs/home-assistant/discussions/539 + "cl_ebt12ypvexnixvtf", # https://github.com/tuya/tuya-home-assistant/issues/754 + "cl_g1cp07dsqnbdbbki", # https://github.com/home-assistant/core/issues/139966 + "cl_qqdxfdht", # https://github.com/orgs/home-assistant/discussions/539 + "cl_zah67ekd", # https://github.com/home-assistant/core/issues/71242 + "clkg_nhyj64w2", # https://github.com/home-assistant/core/issues/136055 + "co2bj_yrr3eiyiacm31ski", # https://github.com/home-assistant/core/issues/133173 + "cobj_hcdy5zrq3ikzthws", # https://github.com/orgs/home-assistant/discussions/482 + "cs_ipmyy4nigpqcnd8q", # https://github.com/home-assistant/core/pull/148726 + "cs_ka2wfrdoogpvgzfi", # https://github.com/home-assistant/core/issues/119865 + "cs_qhxmvae667uap4zh", # https://github.com/home-assistant/core/issues/141278 + "cs_vmxuxszzjwp5smli", # https://github.com/home-assistant/core/issues/119865 + "cs_zibqa9dutqyaxym2", # https://github.com/home-assistant/core/pull/125098 + "cwjwq_agwu93lr", # https://github.com/orgs/home-assistant/discussions/79 + "cwwsq_wfkzyy0evslzsmoi", # https://github.com/home-assistant/core/issues/144745 + "cwysj_akln8rb04cav403q", # https://github.com/home-assistant/core/pull/146599 + "cwysj_z3rpyvznfcch99aa", # https://github.com/home-assistant/core/pull/146599 + "cz_0g1fmqh6d5io7lcn", # https://github.com/home-assistant/core/issues/149704 + "cz_2iepauebcvo74ujc", # https://github.com/home-assistant/core/issues/141278 + "cz_2jxesipczks0kdct", # https://github.com/home-assistant/core/issues/147149 + "cz_37mnhia3pojleqfh", # https://github.com/home-assistant/core/issues/146164 + "cz_39sy2g68gsjwo2xv", # https://github.com/home-assistant/core/issues/141278 + "cz_6fa7odsufen374x2", # https://github.com/home-assistant/core/issues/150029 + "cz_9ivirni8wemum6cw", # https://github.com/home-assistant/core/issues/139735 + "cz_AiHXxAyyn7eAkLQY", # https://github.com/home-assistant/core/issues/150662 + "cz_CHLZe9HQ6QIXujVN", # https://github.com/home-assistant/core/issues/149233 + "cz_HBRBzv1UVBVfF6SL", # https://github.com/tuya/tuya-home-assistant/issues/754 + "cz_anwgf2xugjxpkfxb", # https://github.com/orgs/home-assistant/discussions/539 + "cz_cuhokdii7ojyw8k2", # https://github.com/home-assistant/core/issues/149704 + "cz_dntgh2ngvshfxpsz", # https://github.com/home-assistant/core/issues/149704 + "cz_fencxse0bnut96ig", # https://github.com/home-assistant/core/issues/63978 + "cz_gbtxrqfy9xcsakyp", # https://github.com/home-assistant/core/issues/141278 + "cz_gjnozsaz", # https://github.com/orgs/home-assistant/discussions/482 + "cz_hA2GsgMfTQFTz9JL", # https://github.com/home-assistant/core/issues/148347 + "cz_hj0a5c7ckzzexu8l", # https://github.com/home-assistant/core/issues/149704 + "cz_ik9sbig3mthx9hjz", # https://github.com/home-assistant/core/issues/141278 + "cz_ipabufmlmodje1ws", # https://github.com/home-assistant/core/issues/63978 + "cz_iqhidxhhmgxk5eja", # https://github.com/home-assistant/core/issues/149233 + "cz_jnbbxsb84gvvyfg5", # https://github.com/tuya/tuya-home-assistant/issues/754 + "cz_n8iVBAPLFKAAAszH", # https://github.com/home-assistant/core/issues/146164 + "cz_nkb0fmtlfyqosnvk", # https://github.com/orgs/home-assistant/discussions/482 + "cz_nx8rv6jpe1tsnffk", # https://github.com/home-assistant/core/issues/148347 + "cz_qm0iq4nqnrlzh4qc", # https://github.com/home-assistant/core/issues/141278 + "cz_raceucn29wk2yawe", # https://github.com/tuya/tuya-home-assistant/issues/754 + "cz_sb6bwb1n8ma2c5q4", # https://github.com/home-assistant/core/issues/141278 + "cz_t0a4hwsf8anfsadp", # https://github.com/home-assistant/core/issues/149704 + "cz_tf6qp8t3hl9h7m94", # https://github.com/home-assistant/core/issues/143209 + "cz_tkn2s79mzedk6pwr", # https://github.com/home-assistant/core/issues/146164 + "cz_vxqn72kwtosoy4d3", # https://github.com/home-assistant/core/issues/141278 + "cz_w0qqde0g", # https://github.com/orgs/home-assistant/discussions/482 + "cz_wifvoilfrqeo6hvu", # https://github.com/home-assistant/core/issues/146164 + "cz_wrz6vzch8htux2zp", # https://github.com/home-assistant/core/issues/141278 + "cz_y4jnobxh", # https://github.com/orgs/home-assistant/discussions/482 + "cz_yncyws7tu1q4cpsz", # https://github.com/home-assistant/core/issues/150662 + "cz_z6pht25s3p0gs26q", # https://github.com/home-assistant/core/issues/63978 + "dc_l3bpgg8ibsagon4x", # https://github.com/home-assistant/core/issues/149704 + "dd_gaobbrxqiblcng2p", # https://github.com/home-assistant/core/issues/149233 + "dj_0gyaslysqfp4gfis", # https://github.com/home-assistant/core/issues/149895 + "dj_8szt7whdvwpmxglk", # https://github.com/home-assistant/core/issues/149704 + "dj_8ugheslg", # https://github.com/home-assistant/core/issues/150856 + "dj_8y0aquaa8v6tho8w", # https://github.com/home-assistant/core/issues/149704 + "dj_AqHUMdcbYzIq1Of4", # https://github.com/orgs/home-assistant/discussions/539 + "dj_amx1bgdrfab6jngb", # https://github.com/orgs/home-assistant/discussions/482 + "dj_bSXSSFArVKtc4DyC", # https://github.com/orgs/home-assistant/discussions/539 + "dj_baf9tt9lb8t5uc7z", # https://github.com/home-assistant/core/issues/149704 + "dj_c3nsqogqovapdpfj", # https://github.com/home-assistant/core/issues/146164 + "dj_d4g0fbsoaal841o6", # https://github.com/home-assistant/core/issues/149704 + "dj_dbou1ap4", # https://github.com/orgs/home-assistant/discussions/482 + "dj_djnozmdyqyriow8z", # https://github.com/home-assistant/core/issues/149704 + "dj_ekwolitfjhxn55js", # https://github.com/home-assistant/core/issues/149704 + "dj_fuupmcr2mb1odkja", # https://github.com/home-assistant/core/issues/149704 + "dj_h4aX2JkHZNByQ4AV", # https://github.com/home-assistant/core/issues/150662 + "dj_hp6orhaqm6as3jnv", # https://github.com/home-assistant/core/issues/149704 + "dj_hpc8ddyfv85haxa7", # https://github.com/home-assistant/core/issues/149704 + "dj_iayz2jmtlipjnxj7", # https://github.com/home-assistant/core/issues/149704 + "dj_idnfq7xbx8qewyoa", # https://github.com/home-assistant/core/issues/149704 + "dj_ilddqqih3tucdk68", # https://github.com/home-assistant/core/issues/149704 + "dj_j1bgp31cffutizub", # https://github.com/home-assistant/core/issues/149704 + "dj_kgaob37tz2muf3mi", # https://github.com/home-assistant/core/issues/150856 + "dj_lmnt3uyltk1xffrt", # https://github.com/home-assistant/core/issues/149704 + "dj_mki13ie507rlry4r", # https://github.com/home-assistant/core/pull/126242 + "dj_nbumqpv8vz61enji", # https://github.com/home-assistant/core/issues/149704 + "dj_nlxvjzy1hoeiqsg6", # https://github.com/home-assistant/core/issues/149704 + "dj_oe0cpnjg", # https://github.com/home-assistant/core/issues/149704 + "dj_qoqolwtqzfuhgghq", # https://github.com/home-assistant/core/issues/149233 + "dj_riwp3k79", # https://github.com/home-assistant/core/issues/149704 + "dj_tgewj70aowigv8fz", # https://github.com/orgs/home-assistant/discussions/539 + "dj_tmsloaroqavbucgn", # https://github.com/home-assistant/core/issues/149704 + "dj_ufq2xwuzd4nb0qdr", # https://github.com/home-assistant/core/issues/149704 + "dj_vqwcnabamzrc2kab", # https://github.com/home-assistant/core/issues/149704 + "dj_xdvitmhhmgefaeuq", # https://github.com/home-assistant/core/issues/146164 + "dj_xokdfs6kh5ednakk", # https://github.com/home-assistant/core/issues/149704 + "dj_zakhnlpdiu0ycdxn", # https://github.com/home-assistant/core/issues/149704 + "dj_zav1pa32pyxray78", # https://github.com/home-assistant/core/issues/149704 + "dj_zputiamzanuk6yky", # https://github.com/home-assistant/core/issues/149704 + "dlq_0tnvg2xaisqdadcf", # https://github.com/home-assistant/core/issues/102769 + "dlq_cnpkf4xdmd9v49iq", # https://github.com/home-assistant/core/pull/149320 + "dlq_jdj6ccklup7btq3a", # https://github.com/home-assistant/core/issues/143209 + "dlq_kxdr6su0c55p7bbo", # https://github.com/home-assistant/core/issues/143499 + "dlq_r9kg2g1uhhyicycb", # https://github.com/home-assistant/core/issues/149650 + "dlq_z3jngbyubvwgfrcv", # https://github.com/home-assistant/core/issues/150293 + "dr_pjvxl1wsyqxivsaf", # https://github.com/home-assistant/core/issues/84869 + "fs_g0ewlb1vmwqljzji", # https://github.com/home-assistant/core/issues/141231 + "fs_ibytpo6fpnugft1c", # https://github.com/home-assistant/core/issues/135541 + "fsd_9ecs16c53uqskxw6", # https://github.com/home-assistant/core/issues/149233 + "gyd_lgekqfxdabipm3tn", # https://github.com/home-assistant/core/issues/133173 + "hps_2aaelwxk", # https://github.com/home-assistant/core/issues/149704 + "hps_wqashyqo", # https://github.com/home-assistant/core/issues/146180 + "hwsb_ircs2n82vgrozoew", # https://github.com/home-assistant/core/issues/149233 + "jtmspro_xqeob8h6", # https://github.com/orgs/home-assistant/discussions/517 + "kg_4nqs33emdwJxpQ8O", # https://github.com/orgs/home-assistant/discussions/539 + "kg_5ftkaulg", # https://github.com/orgs/home-assistant/discussions/539 + "kg_gbm9ata1zrzaez4a", # https://github.com/home-assistant/core/issues/148347 + "kj_CAjWAxBUZt7QZHfz", # https://github.com/home-assistant/core/issues/146023 + "kj_fsxtzzhujkrak2oy", # https://github.com/orgs/home-assistant/discussions/439 + "kj_s4uzibibgzdxzowo", # https://github.com/home-assistant/core/issues/150246 + "kj_yrzylxax1qspdgpp", # https://github.com/orgs/home-assistant/discussions/61 + "ks_j9fa8ahzac8uvlfl", # https://github.com/orgs/home-assistant/discussions/329 + "kt_5wnlzekkstwcdsvm", # https://github.com/home-assistant/core/pull/148646 + "kt_ibmmirhhq62mmf1g", # https://github.com/home-assistant/core/pull/150077 + "kt_vdadlnmsorlhw4td", # https://github.com/home-assistant/core/pull/149635 + "ldcg_9kbbfeho", # https://github.com/orgs/home-assistant/discussions/482 + "mal_gyitctrjj1kefxp2", # Alarm Host support + "mc_oSQljE9YDqwCwTUA", # https://github.com/home-assistant/core/issues/149233 + "mcs_6ywsnauy", # https://github.com/orgs/home-assistant/discussions/482 + "mcs_7jIGJAymiH8OsFFb", # https://github.com/home-assistant/core/issues/108301 + "mcs_8yhypbo7", # https://github.com/orgs/home-assistant/discussions/482 + "mcs_hx5ztlztij4yxxvg", # https://github.com/home-assistant/core/issues/148347 + "mcs_qxu3flpqjsc1kqu3", # https://github.com/home-assistant/core/issues/141278 + "mzj_jlapoy5liocmtdvd", # https://github.com/home-assistant/core/issues/150662 + "mzj_qavcakohisj5adyh", # https://github.com/home-assistant/core/issues/141278 + "ntq_9mqdhwklpvnnvb7t", # https://github.com/orgs/home-assistant/discussions/517 + "pc_t2afic7i3v1bwhfp", # https://github.com/home-assistant/core/issues/149704 + "pc_trjopo1vdlt9q1tg", # https://github.com/home-assistant/core/issues/149704 + "pc_tsbguim4trl6fa7g", # https://github.com/home-assistant/core/issues/146164 + "pc_yku9wsimasckdt15", # https://github.com/orgs/home-assistant/discussions/482 + "pir_3amxzozho9xp4mkh", # https://github.com/home-assistant/core/issues/149704 + "pir_fcdjzz3s", # https://github.com/home-assistant/core/issues/149704 + "pir_wqz93nrdomectyoz", # https://github.com/home-assistant/core/issues/149704 + "qccdz_7bvgooyjhiua1yyq", # https://github.com/home-assistant/core/issues/136207 + "qn_5ls2jw49hpczwqng", # https://github.com/home-assistant/core/issues/149233 + "qxj_fsea1lat3vuktbt6", # https://github.com/orgs/home-assistant/discussions/318 + "qxj_is2indt9nlth6esa", # https://github.com/home-assistant/core/issues/136472 + "rqbj_4iqe2hsfyd86kwwc", # https://github.com/orgs/home-assistant/discussions/100 + "sd_i6hyjg3af7doaswm", # https://github.com/orgs/home-assistant/discussions/539 + "sd_lr33znaodtyarrrz", # https://github.com/home-assistant/core/issues/141278 + "sfkzq_1fcnd8xk", # https://github.com/orgs/home-assistant/discussions/539 + "sfkzq_d4vpmigg", # https://github.com/home-assistant/core/issues/150662 + "sfkzq_ed7frwissyqrejic", # https://github.com/home-assistant/core/pull/149236 + "sfkzq_nxquc5lb", # https://github.com/home-assistant/core/issues/150662 + "sfkzq_o6dagifntoafakst", # https://github.com/home-assistant/core/issues/148116 + "sfkzq_rzklytdei8i8vo37", # https://github.com/home-assistant/core/issues/146164 + "sgbj_im2eqqhj72suwwko", # https://github.com/home-assistant/core/issues/151082 + "sgbj_ulv4nnue7gqp0rjk", # https://github.com/home-assistant/core/issues/149704 + "sj_rzeSU2h9uoklxEwq", # https://github.com/home-assistant/core/issues/150683 + "sj_tgvtvdoc", # https://github.com/orgs/home-assistant/discussions/482 + "sp_drezasavompxpcgm", # https://github.com/home-assistant/core/issues/149704 + "sp_nzauwyj3mcnjnf35", # https://github.com/home-assistant/core/issues/141278 + "sp_rjKXWRohlvOTyLBu", # https://github.com/home-assistant/core/issues/149704 + "sp_rudejjigkywujjvs", # https://github.com/home-assistant/core/issues/146164 + "sp_sdd5f5f2dl5wydjf", # https://github.com/home-assistant/core/issues/144087 + "tdq_1aegphq4yfd50e6b", # https://github.com/home-assistant/core/issues/143209 + "tdq_9htyiowaf5rtdhrv", # https://github.com/home-assistant/core/issues/143209 + "tdq_cq1p0nt0a4rixnex", # https://github.com/home-assistant/core/issues/146845 + "tdq_nockvv2k39vbrxxk", # https://github.com/home-assistant/core/issues/145849 + "tdq_pu8uhxhwcp3tgoz7", # https://github.com/home-assistant/core/issues/141278 + "tdq_uoa3mayicscacseb", # https://github.com/home-assistant/core/issues/128911 + "tyndj_pyakuuoc", # https://github.com/home-assistant/core/issues/149704 + "wfcon_b25mh8sxawsgndck", # https://github.com/home-assistant/core/issues/149704 + "wfcon_lieerjyy6l4ykjor", # https://github.com/home-assistant/core/issues/136055 + "wg2_2gowdgni", # https://github.com/home-assistant/core/issues/150856 + "wg2_haclbl0qkqlf2qds", # https://github.com/orgs/home-assistant/discussions/517 + "wg2_nwxr8qcu4seltoro", # https://github.com/orgs/home-assistant/discussions/430 + "wg2_setmxeqgs63xwopm", # https://github.com/orgs/home-assistant/discussions/539 + "wg2_tmwhss6ntjfc7prs", # https://github.com/home-assistant/core/issues/150662 + "wg2_v7owd9tzcaninc36", # https://github.com/orgs/home-assistant/discussions/539 + "wk_6kijc7nd", # https://github.com/home-assistant/core/issues/136513 + "wk_aqoouq7x", # https://github.com/home-assistant/core/issues/146263 + "wk_ccpwojhalfxryigz", # https://github.com/home-assistant/core/issues/145551 + "wk_fi6dne5tu4t1nm6j", # https://github.com/orgs/home-assistant/discussions/243 + "wk_gc1bxoq2hafxpa35", # https://github.com/home-assistant/core/issues/145551 + "wk_gogb05wrtredz3bs", # https://github.com/home-assistant/core/issues/136337 + "wk_y5obtqhuztqsf2mj", # https://github.com/home-assistant/core/issues/139735 + "wkcz_gc4b1mdw7kebtuyz", # https://github.com/home-assistant/core/issues/135617 + "wnykq_npbbca46yiug8ysk", # https://github.com/orgs/home-assistant/discussions/539 + "wnykq_om518smspsaltzdi", # https://github.com/home-assistant/core/issues/150662 + "wnykq_rqhxdyusjrwxyff6", # https://github.com/home-assistant/core/issues/133173 + "wsdcg_g2y6z3p3ja2qhyav", # https://github.com/home-assistant/core/issues/102769 + "wsdcg_iq4ygaai", # https://github.com/orgs/home-assistant/discussions/482 + "wsdcg_iv7hudlj", # https://github.com/home-assistant/core/issues/141278 + "wsdcg_krlcihrpzpc8olw9", # https://github.com/orgs/home-assistant/discussions/517 + "wsdcg_lf36y5nwb8jkxwgg", # https://github.com/orgs/home-assistant/discussions/539 + "wsdcg_vtA4pDd6PLUZzXgZ", # https://github.com/orgs/home-assistant/discussions/482 + "wsdcg_xr3htd96", # https://github.com/orgs/home-assistant/discussions/482 + "wsdcg_yqiqbaldtr0i7mru", # https://github.com/home-assistant/core/issues/136223 + "wxkg_ja5osu5g", # https://github.com/orgs/home-assistant/discussions/482 + "wxkg_l8yaz4um5b3pwyvf", # https://github.com/home-assistant/core/issues/93975 + "ydkt_jevroj5aguwdbs2e", # https://github.com/orgs/home-assistant/discussions/288 + "ygsb_l6ax0u6jwbz82atk", # https://github.com/home-assistant/core/issues/146319 + "ykq_bngwdjsr", # https://github.com/orgs/home-assistant/discussions/482 + "ywbj_arywmw6h6vesoz5t", # https://github.com/home-assistant/core/issues/146164 + "ywbj_cjlutkuuvxnie17o", # https://github.com/home-assistant/core/issues/146164 + "ywbj_gf9dejhmzffgdyfj", # https://github.com/home-assistant/core/issues/149704 + "ywbj_kscbebaf3s1eogvt", # https://github.com/home-assistant/core/issues/141278 + "ywbj_rccxox8p", # https://github.com/orgs/home-assistant/discussions/625 + "ywcgq_h8lvyoahr6s6aybf", # https://github.com/home-assistant/core/issues/145932 + "ywcgq_wtzwyhkev3b4ubns", # https://github.com/home-assistant/core/issues/103818 + "zjq_nkkl7uzv", # https://github.com/orgs/home-assistant/discussions/482 + "zndb_4ggkyflayu1h1ho9", # https://github.com/home-assistant/core/pull/149317 + "zndb_v5jlnn5hwyffkhp3", # https://github.com/home-assistant/core/issues/143209 + "zndb_ze8faryrxr0glqnn", # https://github.com/home-assistant/core/issues/138372 + "znnbq_6b3pbbuqbfabhfiq", # https://github.com/orgs/home-assistant/discussions/707 + "znrb_db81ge24jctwx8lo", # https://github.com/home-assistant/core/issues/136513 + "zwjcy_myd45weu", # https://github.com/orgs/home-assistant/discussions/482 +] class MockDeviceListener(DeviceListener): @@ -501,13 +262,14 @@ async def initialize_entry( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: CustomerDevice | list[CustomerDevice], ) -> None: """Initialize the Tuya component with a mock manager and config entry.""" + if not isinstance(mock_devices, list): + mock_devices = [mock_devices] + mock_manager.device_map = {device.id: device for device in mock_devices} + # Setup - mock_manager.device_map = { - mock_device.id: mock_device, - } mock_config_entry.add_to_hass(hass) # Initialize the component diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index 73752590637..08ede9b73d9 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.json import json_dumps from homeassistant.util import dt as dt_util -from . import MockDeviceListener +from . import DEVICE_MOCKS, MockDeviceListener from tests.common import MockConfigEntry, async_load_json_object_fixture @@ -138,14 +138,34 @@ def mock_device_code() -> str: return None +@pytest.fixture +async def mock_devices(hass: HomeAssistant) -> list[CustomerDevice]: + """Load all Tuya CustomerDevice fixtures. + + Use this to generate global snapshots for each platform. + """ + return [await _create_device(hass, device_code) for device_code in DEVICE_MOCKS] + + @pytest.fixture async def mock_device(hass: HomeAssistant, mock_device_code: str) -> CustomerDevice: + """Load a single Tuya CustomerDevice fixture. + + Use this for testing behavior on a specific device. + """ + return await _create_device(hass, mock_device_code) + + +async def _create_device(hass: HomeAssistant, mock_device_code: str) -> CustomerDevice: """Mock a Tuya CustomerDevice.""" details = await async_load_json_object_fixture( hass, f"{mock_device_code}.json", DOMAIN ) device = MagicMock(spec=CustomerDevice) - device.id = details.get("id", "mocked_device_id") + + # Use reverse of the product_id for testing + device.id = mock_device_code.replace("_", "")[::-1] + device.name = details["name"] device.category = details["category"] device.product_id = details["product_id"] diff --git a/tests/components/tuya/fixtures/bzyd_45idzfufidgee7ir.json b/tests/components/tuya/fixtures/bzyd_45idzfufidgee7ir.json new file mode 100644 index 00000000000..21fc451d392 --- /dev/null +++ b/tests/components/tuya/fixtures/bzyd_45idzfufidgee7ir.json @@ -0,0 +1,137 @@ +{ + "name": "Smart White Noise Machine", + "category": "bzyd", + "product_id": "45idzfufidgee7ir", + "product_name": "Smart White Noise Machine", + "online": true, + "sub": false, + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["scene", "customize_scene", "colour"] + } + }, + "switch_led": { + "type": "Boolean", + "value": {} + }, + "colour_data": { + "type": "Json", + "value": {} + }, + "switch_music": { + "type": "Boolean", + "value": {} + }, + "volume_set": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "stop": { + "type": "Boolean", + "value": {} + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 1440, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["scene", "customize_scene", "colour"] + } + }, + "switch_led": { + "type": "Boolean", + "value": {} + }, + "colour_data": { + "type": "String", + "value": {} + }, + "switch_music": { + "type": "Boolean", + "value": {} + }, + "volume_set": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "stop": { + "type": "Boolean", + "value": {} + }, + "status": { + "type": "Enum", + "value": { + "range": [ + "manual", + "wake_up_1", + "wake_up_2", + "wake_up_3", + "wake_up_4", + "sleep_1", + "sleep_2", + "sleep_3", + "sleep_4" + ] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 1440, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "work_mode": "scene", + "switch_led": true, + "colour_data": { + "h": 240, + "s": 1000, + "v": 1000 + }, + "switch_music": true, + "volume_set": 17, + "stop": false, + "status": "manual", + "countdown": 0 + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/bzyd_ssimhf6r8kgwepfb.json b/tests/components/tuya/fixtures/bzyd_ssimhf6r8kgwepfb.json new file mode 100644 index 00000000000..8490d115409 --- /dev/null +++ b/tests/components/tuya/fixtures/bzyd_ssimhf6r8kgwepfb.json @@ -0,0 +1,76 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "BlissRadia ", + "category": "bzyd", + "product_id": "ssimhf6r8kgwepfb", + "product_name": "BlissRadia ", + "online": true, + "sub": false, + "time_zone": "-04:00", + "active_time": "2025-08-19T14:01:37+00:00", + "create_time": "2025-08-19T14:01:37+00:00", + "update_time": "2025-08-19T14:01:37+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "volume_set": { + "type": "Integer", + "value": { + "unit": "", + "min": 5, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "snooze": { + "type": "Boolean", + "value": {} + }, + "colour_data": { + "type": "Json", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "volume_set": { + "type": "Integer", + "value": { + "unit": "", + "min": 5, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "snooze": { + "type": "Boolean", + "value": {} + }, + "colour_data": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_led": false, + "volume_set": 5, + "snooze": false, + "colour_data": { + "h": 0, + "s": 900, + "v": 1000 + } + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ckmkzq_1yyqfw4djv9eii3q.json b/tests/components/tuya/fixtures/ckmkzq_1yyqfw4djv9eii3q.json new file mode 100644 index 00000000000..ba3bb5b2cf3 --- /dev/null +++ b/tests/components/tuya/fixtures/ckmkzq_1yyqfw4djv9eii3q.json @@ -0,0 +1,59 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Garage door ", + "category": "ckmkzq", + "product_id": "1yyqfw4djv9eii3q", + "product_name": "Garage door ", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2024-07-31T17:45:05+00:00", + "create_time": "2024-07-31T17:45:05+00:00", + "update_time": "2024-07-31T17:45:05+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "doorcontact_state": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": false, + "countdown_1": 0, + "doorcontact_state": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json b/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json index de6c23a1c14..189938aa4f0 100644 --- a/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json +++ b/tests/components/tuya/fixtures/cl_3r8gc33pnqsxfe1g.json @@ -118,6 +118,5 @@ "countdown": "cancel", "countdown_left": 0, "time_total": 25400 - }, - "terminal_id": "REDACTED" + } } diff --git a/tests/components/tuya/fixtures/cl_669wsr2w4cvinbh4.json b/tests/components/tuya/fixtures/cl_669wsr2w4cvinbh4.json new file mode 100644 index 00000000000..de20e242236 --- /dev/null +++ b/tests/components/tuya/fixtures/cl_669wsr2w4cvinbh4.json @@ -0,0 +1,138 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "VIVIDSTORM SCREEN", + "category": "cl", + "product_id": "669wsr2w4cvinbh4", + "product_name": "VIVIDSTORM SCREEN", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2025-08-15T17:34:27+00:00", + "create_time": "2025-08-15T17:34:27+00:00", + "update_time": "2025-08-15T17:34:27+00:00", + "function": { + "control": { + "type": "Enum", + "value": { + "range": ["open", "stop", "close", "continue"] + } + }, + "percent_control": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "control_back_mode": { + "type": "Enum", + "value": { + "range": ["forward", "back"] + } + }, + "border": { + "type": "Enum", + "value": { + "range": ["up", "down", "up_delete", "down_delete", "remove_top_bottom"] + } + } + }, + "status_range": { + "control": { + "type": "Enum", + "value": { + "range": ["open", "stop", "close", "continue"] + } + }, + "percent_control": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "percent_state": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "control_back_mode": { + "type": "Enum", + "value": { + "range": ["forward", "back"] + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["opening", "closing"] + } + }, + "countdown_left": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "time_total": { + "type": "Integer", + "value": { + "unit": "ms", + "min": 0, + "max": 120000, + "scale": 0, + "step": 1 + } + }, + "situation_set": { + "type": "Enum", + "value": { + "range": ["fully_open", "fully_close"] + } + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["motor_fault"] + } + }, + "border": { + "type": "Enum", + "value": { + "range": ["up", "down", "up_delete", "down_delete", "remove_top_bottom"] + } + } + }, + "status": { + "control": "open", + "percent_control": 0, + "percent_state": 0, + "control_back_mode": "forward", + "work_state": "opening", + "countdown_left": 0, + "time_total": 0, + "situation_set": "fully_open", + "fault": 0, + "border": "down" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cl_cpbo62rn.json b/tests/components/tuya/fixtures/cl_cpbo62rn.json index a5ed8e4b580..b52bb31f588 100644 --- a/tests/components/tuya/fixtures/cl_cpbo62rn.json +++ b/tests/components/tuya/fixtures/cl_cpbo62rn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf216113c71bf01a18jtl0", "name": "blinds", "category": "cl", "product_id": "cpbo62rn", diff --git a/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json b/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json index 4b15a27bfd5..fd0ff1fb181 100644 --- a/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json +++ b/tests/components/tuya/fixtures/cl_ebt12ypvexnixvtf.json @@ -53,6 +53,5 @@ }, "status": { "percent_control": 0 - }, - "terminal_id": "REDACTED" + } } diff --git a/tests/components/tuya/fixtures/cl_g1cp07dsqnbdbbki.json b/tests/components/tuya/fixtures/cl_g1cp07dsqnbdbbki.json new file mode 100644 index 00000000000..85f4ec91d4b --- /dev/null +++ b/tests/components/tuya/fixtures/cl_g1cp07dsqnbdbbki.json @@ -0,0 +1,102 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Persiana do Quarto", + "category": "cl", + "product_id": "g1cp07dsqnbdbbki", + "product_name": "Smart roller blinds", + "online": true, + "sub": false, + "time_zone": "-03:00", + "active_time": "2023-06-21T04:29:09+00:00", + "create_time": "2023-06-21T04:29:09+00:00", + "update_time": "2023-06-21T04:29:09+00:00", + "function": { + "control": { + "type": "Enum", + "value": { + "range": ["open", "stop", "close", "continue"] + } + }, + "percent_control": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "control_back_mode": { + "type": "Enum", + "value": { + "range": ["forward", "back"] + } + }, + "border": { + "type": "Enum", + "value": { + "range": ["up", "down", "up_delete"] + } + } + }, + "status_range": { + "control": { + "type": "Enum", + "value": { + "range": ["open", "stop", "close", "continue"] + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["opening", "closing"] + } + }, + "percent_state": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "percent_control": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "control_back_mode": { + "type": "Enum", + "value": { + "range": ["forward", "back"] + } + }, + "border": { + "type": "Enum", + "value": { + "range": ["up", "down", "up_delete"] + } + } + }, + "status": { + "control": "open", + "work_state": "opening", + "percent_state": 0, + "percent_control": 100, + "control_back_mode": "back", + "border": "up" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cl_qqdxfdht.json b/tests/components/tuya/fixtures/cl_qqdxfdht.json index b8f568619db..c0a7bc1d0ba 100644 --- a/tests/components/tuya/fixtures/cl_qqdxfdht.json +++ b/tests/components/tuya/fixtures/cl_qqdxfdht.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb9c4958fd06d141djpqa", "name": "bedroom blinds", "category": "cl", "product_id": "qqdxfdht", diff --git a/tests/components/tuya/fixtures/cl_zah67ekd.json b/tests/components/tuya/fixtures/cl_zah67ekd.json index 14d1c39fc94..b1920f1ecc5 100644 --- a/tests/components/tuya/fixtures/cl_zah67ekd.json +++ b/tests/components/tuya/fixtures/cl_zah67ekd.json @@ -1,5 +1,4 @@ { - "id": "zah67ekd", "name": "Kitchen Blinds", "category": "cl", "product_id": "zah67ekd", diff --git a/tests/components/tuya/fixtures/clkg_nhyj64w2.json b/tests/components/tuya/fixtures/clkg_nhyj64w2.json index 0f64bae778f..1aa6ebebd2c 100644 --- a/tests/components/tuya/fixtures/clkg_nhyj64w2.json +++ b/tests/components/tuya/fixtures/clkg_nhyj64w2.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf1fa053e0ba4e002c6we8", "name": "Tapparelle studio", "category": "clkg", "product_id": "nhyj64w2", diff --git a/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json b/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json index fb544fb7d5e..c4657f30012 100644 --- a/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json +++ b/tests/components/tuya/fixtures/co2bj_yrr3eiyiacm31ski.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb14fd1dd93ca2ea34vpin", "name": "AQI", "category": "co2bj", "product_id": "yrr3eiyiacm31ski", diff --git a/tests/components/tuya/fixtures/cobj_hcdy5zrq3ikzthws.json b/tests/components/tuya/fixtures/cobj_hcdy5zrq3ikzthws.json new file mode 100644 index 00000000000..59e6fc63f1b --- /dev/null +++ b/tests/components/tuya/fixtures/cobj_hcdy5zrq3ikzthws.json @@ -0,0 +1,59 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smogo", + "category": "cobj", + "product_id": "hcdy5zrq3ikzthws", + "product_name": "WIFI smart CO alarm", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2023-07-19T07:41:22+00:00", + "create_time": "2023-07-19T07:41:22+00:00", + "update_time": "2023-07-19T07:41:22+00:00", + "function": {}, + "status_range": { + "co_status": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "co_value": { + "type": "Integer", + "value": { + "unit": "ppm", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "checking_result": { + "type": "Enum", + "value": { + "range": ["checking", "check_success", "check_failure", "others"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "co_status": "normal", + "co_value": 0, + "checking_result": "check_success", + "battery_percentage": 97 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cs_ipmyy4nigpqcnd8q.json b/tests/components/tuya/fixtures/cs_ipmyy4nigpqcnd8q.json new file mode 100644 index 00000000000..816c17e17c7 --- /dev/null +++ b/tests/components/tuya/fixtures/cs_ipmyy4nigpqcnd8q.json @@ -0,0 +1,40 @@ +{ + "name": "Pro Breeze 30L Compressor Dehumidifier", + "category": "cs", + "product_id": "ipmyy4nigpqcnd8q", + "product_name": "30L Dehumidifier with Max Extraction", + "online": true, + "function": { + "anion": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "anion": { + "type": "Boolean", + "value": {} + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["0", "1", "2", "3", "4", "5"] + } + }, + "countdown_left": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 1440, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "anion": false, + "fault": 0, + "countdown_left": 0 + } +} diff --git a/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json b/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json index 755b46fa397..2edd120cf8d 100644 --- a/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json +++ b/tests/components/tuya/fixtures/cs_ka2wfrdoogpvgzfi.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Dehumidifer", "category": "cs", "product_id": "ka2wfrdoogpvgzfi", diff --git a/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json b/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json index 9b0b704e3de..b11dfe88582 100644 --- a/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json +++ b/tests/components/tuya/fixtures/cs_qhxmvae667uap4zh.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "28403630e8db84b7a963", "name": "DryFix", "category": "cs", "product_id": "qhxmvae667uap4zh", diff --git a/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json b/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json index 27d4e825ab1..f4d01c2bc91 100644 --- a/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json +++ b/tests/components/tuya/fixtures/cs_vmxuxszzjwp5smli.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Dehumidifier ", "category": "cs", "product_id": "vmxuxszzjwp5smli", diff --git a/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json b/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json index 5574153a439..fbae30ad3eb 100644 --- a/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json +++ b/tests/components/tuya/fixtures/cs_zibqa9dutqyaxym2.json @@ -1,5 +1,4 @@ { - "id": "bf3fce6af592f12df3gbgq", "name": "Dehumidifier", "category": "cs", "product_id": "zibqa9dutqyaxym2", diff --git a/tests/components/tuya/fixtures/cwjwq_agwu93lr.json b/tests/components/tuya/fixtures/cwjwq_agwu93lr.json index 84f76908338..a421a69bf08 100644 --- a/tests/components/tuya/fixtures/cwjwq_agwu93lr.json +++ b/tests/components/tuya/fixtures/cwjwq_agwu93lr.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf6574iutyikgwkx", "name": "Smart Odor Eliminator-Pro", "category": "cwjwq", "product_id": "agwu93lr", diff --git a/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json b/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json index 4bdd6f3167d..e3858d37602 100644 --- a/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json +++ b/tests/components/tuya/fixtures/cwwsq_wfkzyy0evslzsmoi.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfd0273e59494eb34esvrx", "name": "Cleverio PF100", "category": "cwwsq", "product_id": "wfkzyy0evslzsmoi", diff --git a/tests/components/tuya/fixtures/cwysj_akln8rb04cav403q.json b/tests/components/tuya/fixtures/cwysj_akln8rb04cav403q.json new file mode 100644 index 00000000000..0c13dad643a --- /dev/null +++ b/tests/components/tuya/fixtures/cwysj_akln8rb04cav403q.json @@ -0,0 +1,73 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Water Fountain", + "category": "cwysj", + "product_id": "akln8rb04cav403q", + "product_name": "Smart Pet Water Fountain", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-07-18T11:30:36+00:00", + "create_time": "2025-07-18T11:30:36+00:00", + "update_time": "2025-07-18T11:30:36+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "filter_reset": { + "type": "Boolean", + "value": {} + }, + "pump_reset": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "pump_time": { + "type": "Integer", + "value": { + "unit": "day", + "min": 0, + "max": 31, + "scale": 0, + "step": 1 + } + }, + "filter_reset": { + "type": "Boolean", + "value": {} + }, + "pump_reset": { + "type": "Boolean", + "value": {} + }, + "filter_life": { + "type": "Integer", + "value": { + "unit": "day", + "min": 0, + "max": 30, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": true, + "pump_time": 7, + "filter_reset": false, + "pump_reset": false, + "filter_life": 14 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json b/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json index 695da229041..6f9a8391726 100644 --- a/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json +++ b/tests/components/tuya/fixtures/cwysj_z3rpyvznfcch99aa.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "23536058083a8dc57d96", "name": "PIXI Smart Drinking Fountain", "category": "cwysj", "product_id": "z3rpyvznfcch99aa", diff --git a/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json index 760972e7fb0..8301c806a71 100644 --- a/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json +++ b/tests/components/tuya/fixtures/cz_0g1fmqh6d5io7lcn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "01155072c4dd573f92b8", "name": "Apollo light", "category": "cz", "product_id": "0g1fmqh6d5io7lcn", diff --git a/tests/components/tuya/fixtures/cz_2iepauebcvo74ujc.json b/tests/components/tuya/fixtures/cz_2iepauebcvo74ujc.json new file mode 100644 index 00000000000..e0e41a2ca7e --- /dev/null +++ b/tests/components/tuya/fixtures/cz_2iepauebcvo74ujc.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Aubess Cooker", + "category": "cz", + "product_id": "2iepauebcvo74ujc", + "product_name": "Aubess Smart\u00a0Socket 20A/EM", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-02-03T19:37:03+00:00", + "create_time": "2023-02-03T19:37:03+00:00", + "update_time": "2023-02-03T19:37:03+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 1, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 1574, + "relay_status": "last", + "overcharge_switch": false, + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json b/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json index 27c3ae0c37f..c8191f8a023 100644 --- a/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json +++ b/tests/components/tuya/fixtures/cz_2jxesipczks0kdct.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb0c772dabbb19d653ssi5", "name": "HVAC Meter", "category": "cz", "product_id": "2jxesipczks0kdct", diff --git a/tests/components/tuya/fixtures/cz_37mnhia3pojleqfh.json b/tests/components/tuya/fixtures/cz_37mnhia3pojleqfh.json new file mode 100644 index 00000000000..32ba4caf81a --- /dev/null +++ b/tests/components/tuya/fixtures/cz_37mnhia3pojleqfh.json @@ -0,0 +1,87 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Sapphire ", + "category": "cz", + "product_id": "37mnhia3pojleqfh", + "product_name": "SP111", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-15T13:54:29+00:00", + "create_time": "2025-03-15T13:54:29+00:00", + "update_time": "2025-03-15T13:54:29+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "s", + "max": 86400, + "step": 1 + } + } + }, + "status_range": { + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "s", + "max": 86400, + "step": 1 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "mA", + "max": 30000, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "min": 0, + "unit": "V", + "scale": 0, + "max": 2500, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "W", + "max": 50000, + "step": 1 + } + }, + "switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch": true, + "countdown_1": 0, + "cur_current": 135, + "cur_power": 313, + "cur_voltage": 2357 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_39sy2g68gsjwo2xv.json b/tests/components/tuya/fixtures/cz_39sy2g68gsjwo2xv.json new file mode 100644 index 00000000000..2d067f678f7 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_39sy2g68gsjwo2xv.json @@ -0,0 +1,155 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Ineox SP2", + "category": "cz", + "product_id": "39sy2g68gsjwo2xv", + "product_name": "Ineox SP2", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-03-18T15:08:17+00:00", + "create_time": "2023-03-18T15:08:17+00:00", + "update_time": "2023-03-18T15:08:17+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 3, + "cur_current": 228, + "cur_power": 61, + "cur_voltage": 2321, + "relay_status": "last", + "overcharge_switch": false, + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_6fa7odsufen374x2.json b/tests/components/tuya/fixtures/cz_6fa7odsufen374x2.json new file mode 100644 index 00000000000..0174eb71ca9 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_6fa7odsufen374x2.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Office", + "category": "cz", + "product_id": "6fa7odsufen374x2", + "product_name": "5GHz plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-08-04T14:58:27+00:00", + "create_time": "2025-08-04T14:58:27+00:00", + "update_time": "2025-08-04T14:58:27+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 13, + "cur_current": 253, + "cur_power": 389, + "cur_voltage": 2396, + "relay_status": "last", + "overcharge_switch": false, + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_9ivirni8wemum6cw.json b/tests/components/tuya/fixtures/cz_9ivirni8wemum6cw.json new file mode 100644 index 00000000000..89643d828cf --- /dev/null +++ b/tests/components/tuya/fixtures/cz_9ivirni8wemum6cw.json @@ -0,0 +1,87 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Gar\u00e1\u017e \u010derpadlo", + "category": "cz", + "product_id": "9ivirni8wemum6cw", + "product_name": "Smart Socket", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-07-18T15:41:56+00:00", + "create_time": "2022-07-18T15:41:56+00:00", + "update_time": "2022-07-18T15:41:56+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": true, + "countdown_1": 0, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2407 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json b/tests/components/tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json new file mode 100644 index 00000000000..67510bc7ec3 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Solar Heater Pump", + "category": "cz", + "product_id": "AiHXxAyyn7eAkLQY", + "product_name": "Mini Smart Plug", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2021-07-13T21:37:26+00:00", + "create_time": "2021-07-13T21:37:26+00:00", + "update_time": "2021-07-13T21:37:26+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_CHLZe9HQ6QIXujVN.json b/tests/components/tuya/fixtures/cz_CHLZe9HQ6QIXujVN.json new file mode 100644 index 00000000000..2328e901065 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_CHLZe9HQ6QIXujVN.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "schuur", + "category": "cz", + "product_id": "CHLZe9HQ6QIXujVN", + "product_name": "Smart Plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2019-12-02T17:58:38+00:00", + "create_time": "2019-12-02T17:58:38+00:00", + "update_time": "2019-12-02T17:58:38+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_HBRBzv1UVBVfF6SL.json b/tests/components/tuya/fixtures/cz_HBRBzv1UVBVfF6SL.json new file mode 100644 index 00000000000..8a0ede73696 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_HBRBzv1UVBVfF6SL.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://openapi.tuyaeu.com", + "auth_type": 0, + "country_code": "61", + "app_type": "smartlife", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Rewireable Plug 6930HA", + "model": null, + "category": "cz", + "product_id": "HBRBzv1UVBVfF6SL", + "product_name": "Rewireable Plug 6930HA", + "online": true, + "sub": false, + "time_zone": "+10:00", + "active_time": "2021-07-27T08:43:25+00:00", + "create_time": "2021-07-27T08:43:25+00:00", + "update_time": "2022-02-12T10:40:12+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0 + } +} diff --git a/tests/components/tuya/fixtures/cz_anwgf2xugjxpkfxb.json b/tests/components/tuya/fixtures/cz_anwgf2xugjxpkfxb.json new file mode 100644 index 00000000000..4a22e6f59c0 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_anwgf2xugjxpkfxb.json @@ -0,0 +1,116 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Security Light", + "category": "cz", + "product_id": "anwgf2xugjxpkfxb", + "product_name": "Smart Socket ", + "online": true, + "sub": false, + "time_zone": "+00:00", + "active_time": "2024-04-08T16:20:27+00:00", + "create_time": "2024-04-08T16:20:27+00:00", + "update_time": "2024-04-08T16:20:27+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "relay_status": "power_on", + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json index f259ebd7d6c..8eaecf2407c 100644 --- a/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json +++ b/tests/components/tuya/fixtures/cz_cuhokdii7ojyw8k2.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "53703774d8f15ba9efd3", "name": "Buitenverlichting", "category": "cz", "product_id": "cuhokdii7ojyw8k2", diff --git a/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json index a92d2d370d0..77e19d69a0a 100644 --- a/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json +++ b/tests/components/tuya/fixtures/cz_dntgh2ngvshfxpsz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf7a2cdaf3ce28d2f7uqnh", "name": "fakkel veranda ", "category": "cz", "product_id": "dntgh2ngvshfxpsz", diff --git a/tests/components/tuya/fixtures/cz_fencxse0bnut96ig.json b/tests/components/tuya/fixtures/cz_fencxse0bnut96ig.json new file mode 100644 index 00000000000..a244a6c8bcb --- /dev/null +++ b/tests/components/tuya/fixtures/cz_fencxse0bnut96ig.json @@ -0,0 +1,100 @@ +{ + "endpoint": "https://openapi.tuyaeu.com", + "auth_type": 0, + "country_code": "46", + "app_type": "smartlife", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Spa", + "model": "JLCZ8266", + "category": "cz", + "product_id": "fencxse0bnut96ig", + "product_name": "smart plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-01-28T07:32:38+00:00", + "create_time": "2022-01-28T07:32:38+00:00", + "update_time": "2022-01-28T07:32:52+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 203, + "cur_current": 5404, + "cur_power": 12018, + "cur_voltage": 2381 + } +} diff --git a/tests/components/tuya/fixtures/cz_gbtxrqfy9xcsakyp.json b/tests/components/tuya/fixtures/cz_gbtxrqfy9xcsakyp.json new file mode 100644 index 00000000000..456c2b1fa60 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_gbtxrqfy9xcsakyp.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "3DPrinter", + "category": "cz", + "product_id": "gbtxrqfy9xcsakyp", + "product_name": "Smart Plug+", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2023-08-16T08:48:56+00:00", + "create_time": "2023-08-16T08:48:56+00:00", + "update_time": "2023-08-16T08:48:56+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": false, + "countdown_1": 0, + "add_ele": 1, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2319, + "relay_status": "last", + "overcharge_switch": false, + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_gjnozsaz.json b/tests/components/tuya/fixtures/cz_gjnozsaz.json new file mode 100644 index 00000000000..dab20faf3c7 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_gjnozsaz.json @@ -0,0 +1,133 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Raspy4 - Home Assistant", + "category": "cz", + "product_id": "gjnozsaz", + "product_name": "Smart plug", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T09:52:30+00:00", + "create_time": "2025-07-19T09:52:30+00:00", + "update_time": "2025-07-19T09:52:30+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 100000000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 1500, + "cur_current": 33, + "cur_power": 30, + "cur_voltage": 2440, + "relay_status": "power_on", + "light_mode": "none", + "child_lock": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_hA2GsgMfTQFTz9JL.json b/tests/components/tuya/fixtures/cz_hA2GsgMfTQFTz9JL.json new file mode 100644 index 00000000000..d0d7001fc02 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_hA2GsgMfTQFTz9JL.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": true, + "name": "Spot 4", + "category": "cz", + "product_id": "hA2GsgMfTQFTz9JL", + "product_name": "Mini Smart Socket", + "online": true, + "sub": false, + "time_zone": "-04:00", + "active_time": "2025-06-19T17:17:15+00:00", + "create_time": "2025-06-19T17:17:15+00:00", + "update_time": "2025-06-19T17:17:15+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json index 0638bb02d1e..b40297eab8f 100644 --- a/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json +++ b/tests/components/tuya/fixtures/cz_hj0a5c7ckzzexu8l.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "051724052462ab286504", "name": "droger", "category": "cz", "product_id": "hj0a5c7ckzzexu8l", diff --git a/tests/components/tuya/fixtures/cz_ik9sbig3mthx9hjz.json b/tests/components/tuya/fixtures/cz_ik9sbig3mthx9hjz.json new file mode 100644 index 00000000000..905f4270d18 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_ik9sbig3mthx9hjz.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Aubess Washing Machine", + "category": "cz", + "product_id": "ik9sbig3mthx9hjz", + "product_name": "Aubess Smart\u00a0Socket EM", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2024-08-18T10:29:20+00:00", + "create_time": "2024-08-18T10:29:20+00:00", + "update_time": "2024-08-18T10:29:20+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 1, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2299, + "relay_status": "last", + "overcharge_switch": false, + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_ipabufmlmodje1ws.json b/tests/components/tuya/fixtures/cz_ipabufmlmodje1ws.json new file mode 100644 index 00000000000..5edf6500132 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_ipabufmlmodje1ws.json @@ -0,0 +1,165 @@ +{ + "endpoint": "https://openapi.tuyaeu.com", + "auth_type": 0, + "country_code": "46", + "app_type": "smartlife", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "V\u00e4rmelampa", + "model": "FK-PW802EC-F", + "category": "cz", + "product_id": "ipabufmlmodje1ws", + "product_name": "", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-01-22T12:55:50+00:00", + "create_time": "2022-01-21T20:32:42+00:00", + "update_time": "2022-01-22T12:55:53+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 0, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "test_bit": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 5, + "scale": 0, + "step": 1 + } + }, + "voltage_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "electric_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "power_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "electricity_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 82, + "cur_current": 435, + "cur_power": 1642, + "cur_voltage": 2246, + "test_bit": 1, + "voltage_coe": 632, + "electric_coe": 10795, + "power_coe": 10197, + "electricity_coe": 4090 + } +} diff --git a/tests/components/tuya/fixtures/cz_iqhidxhhmgxk5eja.json b/tests/components/tuya/fixtures/cz_iqhidxhhmgxk5eja.json new file mode 100644 index 00000000000..958d400eb0e --- /dev/null +++ b/tests/components/tuya/fixtures/cz_iqhidxhhmgxk5eja.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Powerplug 5", + "category": "cz", + "product_id": "iqhidxhhmgxk5eja", + "product_name": "Smart Plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2020-05-25T11:34:43+00:00", + "create_time": "2020-05-25T11:34:43+00:00", + "update_time": "2020-05-25T11:34:43+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_jnbbxsb84gvvyfg5.json b/tests/components/tuya/fixtures/cz_jnbbxsb84gvvyfg5.json new file mode 100644 index 00000000000..03edf52d3c4 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_jnbbxsb84gvvyfg5.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://openapi.tuyaeu.com", + "auth_type": 0, + "country_code": "61", + "app_type": "smartlife", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Bathroom Fan", + "model": "6920HA", + "category": "cz", + "product_id": "jnbbxsb84gvvyfg5", + "product_name": "Plug Base 6210HA", + "online": true, + "sub": false, + "time_zone": "+10:00", + "active_time": "2021-08-23T20:40:36+00:00", + "create_time": "2021-08-18T13:14:59+00:00", + "update_time": "2022-02-12T10:40:14+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0 + } +} diff --git a/tests/components/tuya/fixtures/cz_n8iVBAPLFKAAAszH.json b/tests/components/tuya/fixtures/cz_n8iVBAPLFKAAAszH.json new file mode 100644 index 00000000000..4de85bb849f --- /dev/null +++ b/tests/components/tuya/fixtures/cz_n8iVBAPLFKAAAszH.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Steckdose 2", + "category": "cz", + "product_id": "n8iVBAPLFKAAAszH", + "product_name": "Socket", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-15T13:17:22+00:00", + "create_time": "2025-03-15T13:17:22+00:00", + "update_time": "2025-03-15T13:17:22+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_nkb0fmtlfyqosnvk.json b/tests/components/tuya/fixtures/cz_nkb0fmtlfyqosnvk.json new file mode 100644 index 00000000000..f0fe165ecb8 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_nkb0fmtlfyqosnvk.json @@ -0,0 +1,98 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Bassin", + "category": "cz", + "product_id": "nkb0fmtlfyqosnvk", + "product_name": "Konyks Pluviose Easy EU", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2024-04-11T16:27:36+00:00", + "create_time": "2024-04-11T16:27:36+00:00", + "update_time": "2024-04-11T16:27:36+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 21, + "cur_current": 783, + "cur_power": 411, + "cur_voltage": 2454 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_nx8rv6jpe1tsnffk.json b/tests/components/tuya/fixtures/cz_nx8rv6jpe1tsnffk.json new file mode 100644 index 00000000000..277a2fbe81e --- /dev/null +++ b/tests/components/tuya/fixtures/cz_nx8rv6jpe1tsnffk.json @@ -0,0 +1,116 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": true, + "name": "Spot 1", + "category": "cz", + "product_id": "nx8rv6jpe1tsnffk", + "product_name": "Smart plug", + "online": true, + "sub": false, + "time_zone": "-04:00", + "active_time": "2025-06-21T17:03:23+00:00", + "create_time": "2025-06-21T17:03:23+00:00", + "update_time": "2025-06-21T17:03:23+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": false, + "countdown_1": 0, + "relay_status": "last", + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_qm0iq4nqnrlzh4qc.json b/tests/components/tuya/fixtures/cz_qm0iq4nqnrlzh4qc.json new file mode 100644 index 00000000000..167878abc59 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_qm0iq4nqnrlzh4qc.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Elivco Kitchen Socket", + "category": "cz", + "product_id": "qm0iq4nqnrlzh4qc", + "product_name": "Smart plug", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2023-03-29T15:03:22+00:00", + "create_time": "2023-03-29T15:03:22+00:00", + "update_time": "2023-03-29T15:03:22+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 24, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2334, + "relay_status": "power_on", + "overcharge_switch": false, + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_raceucn29wk2yawe.json b/tests/components/tuya/fixtures/cz_raceucn29wk2yawe.json new file mode 100644 index 00000000000..a77bfd79d6e --- /dev/null +++ b/tests/components/tuya/fixtures/cz_raceucn29wk2yawe.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://openapi.tuyaeu.com", + "auth_type": 0, + "country_code": "61", + "app_type": "smartlife", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Bathroom Mirror", + "model": "", + "category": "cz", + "product_id": "raceucn29wk2yawe", + "product_name": "Inline Switch 6000HA", + "online": true, + "sub": false, + "time_zone": "+10:00", + "active_time": "2021-08-18T13:03:59+00:00", + "create_time": "2021-08-18T13:03:59+00:00", + "update_time": "2022-02-12T10:40:14+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0 + } +} diff --git a/tests/components/tuya/fixtures/cz_sb6bwb1n8ma2c5q4.json b/tests/components/tuya/fixtures/cz_sb6bwb1n8ma2c5q4.json new file mode 100644 index 00000000000..b077af094fa --- /dev/null +++ b/tests/components/tuya/fixtures/cz_sb6bwb1n8ma2c5q4.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Socket4", + "category": "cz", + "product_id": "sb6bwb1n8ma2c5q4", + "product_name": "WIFI \u63d2\u5ea7", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-01-16T18:50:14+00:00", + "create_time": "2025-01-16T18:50:14+00:00", + "update_time": "2025-01-16T18:50:14+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": false, + "countdown_1": 0, + "add_ele": 0, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2325, + "relay_status": "last", + "overcharge_switch": false, + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json index b7f913a7153..04a2d12e853 100644 --- a/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json +++ b/tests/components/tuya/fixtures/cz_t0a4hwsf8anfsadp.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf4c0c538bfe408aa9gr2e", "name": "wallwasher front", "category": "cz", "product_id": "t0a4hwsf8anfsadp", diff --git a/tests/components/tuya/fixtures/cz_tf6qp8t3hl9h7m94.json b/tests/components/tuya/fixtures/cz_tf6qp8t3hl9h7m94.json new file mode 100644 index 00000000000..22db23d06dc --- /dev/null +++ b/tests/components/tuya/fixtures/cz_tf6qp8t3hl9h7m94.json @@ -0,0 +1,86 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Consommation", + "category": "cz", + "product_id": "tf6qp8t3hl9h7m94", + "product_name": "smart meter with CT-2", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-04-18T08:10:00+00:00", + "create_time": "2025-04-18T08:10:00+00:00", + "update_time": "2025-04-18T08:10:00+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "A", + "min": 0, + "max": 80000, + "scale": 3, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 200000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "switch_2": false, + "add_ele": 100, + "cur_current": 2585, + "cur_power": 4258, + "cur_voltage": 2416 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_tkn2s79mzedk6pwr.json b/tests/components/tuya/fixtures/cz_tkn2s79mzedk6pwr.json new file mode 100644 index 00000000000..4a551736c3f --- /dev/null +++ b/tests/components/tuya/fixtures/cz_tkn2s79mzedk6pwr.json @@ -0,0 +1,160 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Weihnachtsmann ", + "category": "cz", + "product_id": "tkn2s79mzedk6pwr", + "product_name": "Smart Socket ", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-11-28T18:55:54+00:00", + "create_time": "2023-11-28T18:55:54+00:00", + "update_time": "2023-11-28T18:55:54+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 80000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 200000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": false, + "countdown_1": 0, + "add_ele": 4, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 0, + "relay_status": "last", + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_vxqn72kwtosoy4d3.json b/tests/components/tuya/fixtures/cz_vxqn72kwtosoy4d3.json new file mode 100644 index 00000000000..3b4b98514ba --- /dev/null +++ b/tests/components/tuya/fixtures/cz_vxqn72kwtosoy4d3.json @@ -0,0 +1,98 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Garage Socket", + "category": "cz", + "product_id": "vxqn72kwtosoy4d3", + "product_name": "Smart Plug+", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-01-11T13:38:52+00:00", + "create_time": "2024-01-11T13:38:52+00:00", + "update_time": "2024-01-11T13:38:52+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 80000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 200000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 2, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2350 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_w0qqde0g.json b/tests/components/tuya/fixtures/cz_w0qqde0g.json new file mode 100644 index 00000000000..6d960603ba1 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_w0qqde0g.json @@ -0,0 +1,133 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Lave linge", + "category": "cz", + "product_id": "w0qqde0g", + "product_name": "Smart plug", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T08:38:51+00:00", + "create_time": "2025-07-19T08:38:51+00:00", + "update_time": "2025-07-19T08:38:51+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 100000000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 62860, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2440, + "relay_status": "power_on", + "light_mode": "none", + "child_lock": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_wifvoilfrqeo6hvu.json b/tests/components/tuya/fixtures/cz_wifvoilfrqeo6hvu.json new file mode 100644 index 00000000000..e0912445003 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_wifvoilfrqeo6hvu.json @@ -0,0 +1,98 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Licht drucker", + "category": "cz", + "product_id": "wifvoilfrqeo6hvu", + "product_name": "Smart Socket", + "online": false, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-15T13:48:41+00:00", + "create_time": "2025-03-15T13:48:41+00:00", + "update_time": "2025-03-15T13:48:41+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "\u79d2", + "max": 86400, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "\u79d2", + "max": 86400, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "\u5ea6", + "max": 500000, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "mA", + "max": 30000, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "W", + "max": 50000, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "min": 0, + "unit": "V", + "scale": 0, + "max": 2500, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 10, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2346 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_wrz6vzch8htux2zp.json b/tests/components/tuya/fixtures/cz_wrz6vzch8htux2zp.json new file mode 100644 index 00000000000..29cb9488745 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_wrz6vzch8htux2zp.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Elivco TV", + "category": "cz", + "product_id": "wrz6vzch8htux2zp", + "product_name": "WiFi Plug", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-02-20T12:10:18+00:00", + "create_time": "2023-02-20T12:10:18+00:00", + "update_time": "2023-02-20T12:10:18+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "overcharge_switch": { + "type": "Boolean", + "value": {} + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "countdown_1": 0, + "add_ele": 6, + "cur_current": 91, + "cur_power": 100, + "cur_voltage": 2377, + "relay_status": "last", + "overcharge_switch": false, + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_y4jnobxh.json b/tests/components/tuya/fixtures/cz_y4jnobxh.json new file mode 100644 index 00000000000..27680e4521a --- /dev/null +++ b/tests/components/tuya/fixtures/cz_y4jnobxh.json @@ -0,0 +1,33 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "AuVeLiCo", + "category": "cz", + "product_id": "y4jnobxh", + "product_name": "\u3010\u901a\u7528\u63a5\u5165\u30111\u8def\u63d2\u5ea7", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T09:30:43+00:00", + "create_time": "2025-07-19T09:30:43+00:00", + "update_time": "2025-07-19T09:30:43+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_yncyws7tu1q4cpsz.json b/tests/components/tuya/fixtures/cz_yncyws7tu1q4cpsz.json new file mode 100644 index 00000000000..c0cfa202a50 --- /dev/null +++ b/tests/components/tuya/fixtures/cz_yncyws7tu1q4cpsz.json @@ -0,0 +1,37 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Wi-Fi hub", + "category": "cz", + "product_id": "yncyws7tu1q4cpsz", + "product_name": "Wi-Fi hub", + "online": true, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-09T18:54:03+00:00", + "create_time": "2025-08-09T18:54:03+00:00", + "update_time": "2025-08-09T18:54:03+00:00", + "function": { + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + } + }, + "status_range": { + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + } + }, + "status": { + "relay_status": "power_on" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_z6pht25s3p0gs26q.json b/tests/components/tuya/fixtures/cz_z6pht25s3p0gs26q.json new file mode 100644 index 00000000000..01caa14439a --- /dev/null +++ b/tests/components/tuya/fixtures/cz_z6pht25s3p0gs26q.json @@ -0,0 +1,207 @@ +{ + "endpoint": "https://openapi.tuyaeu.com", + "auth_type": 0, + "country_code": "61", + "app_type": "tuyaSmart", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "6294HA", + "model": "", + "category": "cz", + "product_id": "z6pht25s3p0gs26q", + "product_name": "6294HA", + "online": true, + "sub": false, + "time_zone": "+10:00", + "active_time": "2022-01-20T11:03:22+00:00", + "create_time": "2022-01-10T01:30:10+00:00", + "update_time": "2022-01-20T11:03:22+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "kwh", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 0, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "test_bit": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 5, + "scale": 0, + "step": 1 + } + }, + "voltage_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "electric_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "power_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "electricity_coe": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": true, + "switch_2": false, + "countdown_1": 0, + "countdown_2": 0, + "add_ele": 201, + "cur_current": 5466, + "cur_power": 11374, + "cur_voltage": 2396, + "test_bit": 2, + "voltage_coe": 0, + "electric_coe": 0, + "power_coe": 0, + "electricity_coe": 0, + "relay_status": "power_on", + "child_lock": false + } +} diff --git a/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json index b3759178618..198a2462ad1 100644 --- a/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json +++ b/tests/components/tuya/fixtures/dc_l3bpgg8ibsagon4x.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfd9f45c6b882c9f46dxfc", "name": "LSC Party String Light RGBIC+CCT ", "category": "dc", "product_id": "l3bpgg8ibsagon4x", diff --git a/tests/components/tuya/fixtures/dd_gaobbrxqiblcng2p.json b/tests/components/tuya/fixtures/dd_gaobbrxqiblcng2p.json new file mode 100644 index 00000000000..b0135acba1c --- /dev/null +++ b/tests/components/tuya/fixtures/dd_gaobbrxqiblcng2p.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "TV Sync Backlights", + "category": "dd", + "product_id": "gaobbrxqiblcng2p", + "product_name": "TV Sync Backlights", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2024-08-31T10:40:08+00:00", + "create_time": "2024-08-31T10:40:08+00:00", + "update_time": "2024-08-31T10:40:08+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_0gyaslysqfp4gfis.json b/tests/components/tuya/fixtures/dj_0gyaslysqfp4gfis.json new file mode 100644 index 00000000000..3ab0f17cb9d --- /dev/null +++ b/tests/components/tuya/fixtures/dj_0gyaslysqfp4gfis.json @@ -0,0 +1,557 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Study 1", + "category": "dj", + "product_id": "0gyaslysqfp4gfis", + "product_name": "", + "online": true, + "sub": false, + "time_zone": "+10:00", + "active_time": "2025-06-29T07:45:01+00:00", + "create_time": "2025-06-29T07:45:01+00:00", + "update_time": "2025-06-29T07:45:01+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "cycle_timing": { + "type": "Raw", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAAAPoA+gD6APo", + "do_not_disturb": true, + "remote_switch": true, + "cycle_timing": "AAAA", + "random_timing": "AAAA" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json index 6cd0ca55379..8b6e491fa43 100644 --- a/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json +++ b/tests/components/tuya/fixtures/dj_8szt7whdvwpmxglk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb10549aadfc74b7c8q2ti", "name": "Porch light E", "category": "dj", "product_id": "8szt7whdvwpmxglk", diff --git a/tests/components/tuya/fixtures/dj_8ugheslg.json b/tests/components/tuya/fixtures/dj_8ugheslg.json new file mode 100644 index 00000000000..870618789c2 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_8ugheslg.json @@ -0,0 +1,57 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "POWERASIA R2", + "category": "dj", + "product_id": "8ugheslg", + "product_name": "POWERASIA", + "online": true, + "sub": true, + "time_zone": "-05:00", + "active_time": "2024-07-27T23:47:47+00:00", + "create_time": "2024-07-27T23:47:47+00:00", + "update_time": "2024-07-27T23:47:47+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json index ec8f6a0a4d5..d2e36e71f49 100644 --- a/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json +++ b/tests/components/tuya/fixtures/dj_8y0aquaa8v6tho8w.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf71858c3d27943679dsx9", "name": "dressoir spot", "category": "dj", "product_id": "8y0aquaa8v6tho8w", diff --git a/tests/components/tuya/fixtures/dj_AqHUMdcbYzIq1Of4.json b/tests/components/tuya/fixtures/dj_AqHUMdcbYzIq1Of4.json new file mode 100644 index 00000000000..8e54b45ee68 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_AqHUMdcbYzIq1Of4.json @@ -0,0 +1,508 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Landing", + "category": "dj", + "product_id": "AqHUMdcbYzIq1Of4", + "product_name": "Smart Bulb", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-04-28T06:04:01+00:00", + "create_time": "2023-04-28T06:04:01+00:00", + "update_time": "2023-04-28T06:04:01+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "flash_scene_1": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_2": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_3": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour"] + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "scene_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_4": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "scene_data": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_2": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour"] + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "flash_scene_1": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_3": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "flash_scene_4": { + "type": "Json", + "value": { + "h": { + "min": 1, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 1, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value": 255, + "temp_value": 127, + "colour_data": { + "h": 1.0, + "s": 1.0, + "v": 255.0 + }, + "scene_data": { + "h": 0.0, + "s": 0.0, + "v": 0.0 + }, + "flash_scene_1": { + "bright": 255, + "frequency": 80, + "hsv": [ + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + } + ], + "temperature": 255 + }, + "flash_scene_2": { + "bright": 255, + "frequency": 128, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 240.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + } + ], + "temperature": 255 + }, + "flash_scene_3": { + "bright": 255, + "frequency": 80, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + } + ], + "temperature": 255 + }, + "flash_scene_4": { + "bright": 255, + "frequency": 5, + "hsv": [ + { + "h": 0.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 120.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 60.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 300.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 240.0, + "s": 255.0, + "v": 255.0 + }, + { + "h": 0.0, + "s": 0.0, + "v": 0.0 + } + ], + "temperature": 255 + } + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_amx1bgdrfab6jngb.json b/tests/components/tuya/fixtures/dj_amx1bgdrfab6jngb.json new file mode 100644 index 00000000000..1978a729b1a --- /dev/null +++ b/tests/components/tuya/fixtures/dj_amx1bgdrfab6jngb.json @@ -0,0 +1,333 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Lumy Hall", + "category": "dj", + "product_id": "amx1bgdrfab6jngb", + "product_name": "A60 Clear", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-05-20T16:20:25+00:00", + "create_time": "2024-05-20T16:20:25+00:00", + "update_time": "2024-05-20T16:20:25+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_bSXSSFArVKtc4DyC.json b/tests/components/tuya/fixtures/dj_bSXSSFArVKtc4DyC.json new file mode 100644 index 00000000000..a0e9027e70c --- /dev/null +++ b/tests/components/tuya/fixtures/dj_bSXSSFArVKtc4DyC.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "bedroom", + "category": "dj", + "product_id": "bSXSSFArVKtc4DyC", + "product_name": "Dimmer switch", + "online": true, + "sub": false, + "time_zone": "+00:00", + "active_time": "2023-04-28T05:43:06+00:00", + "create_time": "2023-04-28T05:43:06+00:00", + "update_time": "2023-04-28T05:43:06+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 11 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json index 211c0bc12cf..86d1f8fd9d5 100644 --- a/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json +++ b/tests/components/tuya/fixtures/dj_baf9tt9lb8t5uc7z.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "40611462e09806c73134", "name": "Pokerlamp 2", "category": "dj", "product_id": "baf9tt9lb8t5uc7z", diff --git a/tests/components/tuya/fixtures/dj_c3nsqogqovapdpfj.json b/tests/components/tuya/fixtures/dj_c3nsqogqovapdpfj.json new file mode 100644 index 00000000000..c5a1aefec54 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_c3nsqogqovapdpfj.json @@ -0,0 +1,348 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Arbeitszimmer led", + "category": "dj", + "product_id": "c3nsqogqovapdpfj", + "product_name": "RGBstriplight", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-15T13:33:52+00:00", + "create_time": "2025-03-15T13:33:52+00:00", + "update_time": "2025-03-15T13:33:52+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": true, + "work_mode": "scene", + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 2, + "scene_units": [ + { + "bright": 0, + "h": 132, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 1000 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json index 22650f7ae37..024501d59de 100644 --- a/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json +++ b/tests/components/tuya/fixtures/dj_d4g0fbsoaal841o6.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf671413db4cee1f9bqdcx", "name": "WC D1", "category": "dj", "product_id": "d4g0fbsoaal841o6", diff --git a/tests/components/tuya/fixtures/dj_dbou1ap4.json b/tests/components/tuya/fixtures/dj_dbou1ap4.json new file mode 100644 index 00000000000..86f16136678 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_dbou1ap4.json @@ -0,0 +1,390 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Lumy Garage", + "category": "dj", + "product_id": "dbou1ap4", + "product_name": "atmosphere", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T11:19:47+00:00", + "create_time": "2025-07-19T11:19:47+00:00", + "update_time": "2025-07-19T11:19:47+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 1000, + "colour_data_v2": { + "h": 186, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 13, + "v": 0 + } + ] + }, + "countdown_1": 0, + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json index 67df13c674f..d48e7228566 100644 --- a/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json +++ b/tests/components/tuya/fixtures/dj_djnozmdyqyriow8z.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf8885f3d18a73e395bfac", "name": "Fakkel 8", "category": "dj", "product_id": "djnozmdyqyriow8z", diff --git a/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json index 90cad22fd09..ae3a53e606e 100644 --- a/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json +++ b/tests/components/tuya/fixtures/dj_ekwolitfjhxn55js.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb99bba00c9c90ba8gzgl", "name": "ab6", "category": "dj", "product_id": "ekwolitfjhxn55js", diff --git a/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json index 5b189b6a3e4..39cb6b78460 100644 --- a/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json +++ b/tests/components/tuya/fixtures/dj_fuupmcr2mb1odkja.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0914a82b06ecf151xsf5", "name": "Slaapkamer", "category": "dj", "product_id": "fuupmcr2mb1odkja", diff --git a/tests/components/tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json b/tests/components/tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json new file mode 100644 index 00000000000..0f790ecfc34 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Entry Stairs", + "category": "dj", + "product_id": "h4aX2JkHZNByQ4AV", + "product_name": "Smart Dimmer Switch", + "online": false, + "sub": false, + "time_zone": "-05:00", + "active_time": "2023-04-25T13:21:00+00:00", + "create_time": "2023-04-25T13:21:00+00:00", + "update_time": "2023-04-25T13:21:00+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 64 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json index e8166a192dc..22e5eee1b6f 100644 --- a/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json +++ b/tests/components/tuya/fixtures/dj_hp6orhaqm6as3jnv.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "00450321483fda81c529", "name": "Master bedroom TV lights", "category": "dj", "product_id": "hp6orhaqm6as3jnv", diff --git a/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json index 893aafa3759..b7190caa78e 100644 --- a/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json +++ b/tests/components/tuya/fixtures/dj_hpc8ddyfv85haxa7.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "63362034840d8eb9029f", "name": "Garage", "category": "dj", "product_id": "hpc8ddyfv85haxa7", diff --git a/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json index f9062d9146d..a8cddb4ee4f 100644 --- a/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json +++ b/tests/components/tuya/fixtures/dj_iayz2jmtlipjnxj7.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0fc1d7d4caa71a59us7c", "name": "LED Porch 2", "category": "dj", "product_id": "iayz2jmtlipjnxj7", diff --git a/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json index 295157d8370..299e8d573f1 100644 --- a/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json +++ b/tests/components/tuya/fixtures/dj_idnfq7xbx8qewyoa.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf599f5cffe1a5985depyk", "name": "AB1", "category": "dj", "product_id": "idnfq7xbx8qewyoa", diff --git a/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json index 1181b650f3e..affa875f3b4 100644 --- a/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json +++ b/tests/components/tuya/fixtures/dj_ilddqqih3tucdk68.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "84178216d8f15be52dc4", "name": "Ieskas", "category": "dj", "product_id": "ilddqqih3tucdk68", diff --git a/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json index d95179c921f..01c7e375002 100644 --- a/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json +++ b/tests/components/tuya/fixtures/dj_j1bgp31cffutizub.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfe49d7b6cd80536efdldi", "name": "Ceiling Portal", "category": "dj", "product_id": "j1bgp31cffutizub", diff --git a/tests/components/tuya/fixtures/dj_kgaob37tz2muf3mi.json b/tests/components/tuya/fixtures/dj_kgaob37tz2muf3mi.json new file mode 100644 index 00000000000..36a2721c58e --- /dev/null +++ b/tests/components/tuya/fixtures/dj_kgaob37tz2muf3mi.json @@ -0,0 +1,548 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Parker Ceiling Fan 1", + "category": "dj", + "product_id": "kgaob37tz2muf3mi", + "product_name": "", + "online": false, + "sub": false, + "time_zone": "-05:00", + "active_time": "2024-04-05T01:27:07+00:00", + "create_time": "2024-04-05T01:27:07+00:00", + "update_time": "2024-04-05T01:27:07+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 300, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAAAPoA+gD6AEs", + "do_not_disturb": false, + "remote_switch": true, + "random_timing": "AAAA" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json index 93a802a7ee3..54c08ba7762 100644 --- a/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json +++ b/tests/components/tuya/fixtures/dj_lmnt3uyltk1xffrt.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "07608286600194e94248", "name": "DirectietKamer", "category": "dj", "product_id": "lmnt3uyltk1xffrt", diff --git a/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json b/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json index 49854adc889..daea124e8e0 100644 --- a/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json +++ b/tests/components/tuya/fixtures/dj_mki13ie507rlry4r.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "REDACTED", "name": "Garage light", "category": "dj", "product_id": "mki13ie507rlry4r", diff --git a/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json index bc919dd92d2..3cac3935c27 100644 --- a/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json +++ b/tests/components/tuya/fixtures/dj_nbumqpv8vz61enji.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf77c04cbd6a52a7be16ll", "name": "b2", "category": "dj", "product_id": "nbumqpv8vz61enji", diff --git a/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json index c519f1aa593..5fbea6fb287 100644 --- a/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json +++ b/tests/components/tuya/fixtures/dj_nlxvjzy1hoeiqsg6.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "40350105dc4f229a464e", "name": "hall 💡 ", "category": "dj", "product_id": "nlxvjzy1hoeiqsg6", diff --git a/tests/components/tuya/fixtures/dj_oe0cpnjg.json b/tests/components/tuya/fixtures/dj_oe0cpnjg.json index 646ce8a93d7..8c2a559a5c9 100644 --- a/tests/components/tuya/fixtures/dj_oe0cpnjg.json +++ b/tests/components/tuya/fixtures/dj_oe0cpnjg.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf8d8af3ddfe75b0195r0h", "name": "Front right Lighting trap", "category": "dj", "product_id": "oe0cpnjg", diff --git a/tests/components/tuya/fixtures/dj_qoqolwtqzfuhgghq.json b/tests/components/tuya/fixtures/dj_qoqolwtqzfuhgghq.json new file mode 100644 index 00000000000..e623ac6f7c0 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_qoqolwtqzfuhgghq.json @@ -0,0 +1,477 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smart Bulb RGBCW", + "category": "dj", + "product_id": "qoqolwtqzfuhgghq", + "product_name": "Smart Bulb RGBCW", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-01-04T09:40:06+00:00", + "create_time": "2022-01-04T09:40:06+00:00", + "update_time": "2022-01-04T09:40:06+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 435, + "colour_data_v2": { + "h": 35, + "s": 760, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 1000, + "h": 0, + "s": 0, + "temperature": 85, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_riwp3k79.json b/tests/components/tuya/fixtures/dj_riwp3k79.json index f1a3579e660..bd4d013ab5b 100644 --- a/tests/components/tuya/fixtures/dj_riwp3k79.json +++ b/tests/components/tuya/fixtures/dj_riwp3k79.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf46b2b81ca41ce0c1xpsw", "name": "LED KEUKEN 2", "category": "dj", "product_id": "riwp3k79", diff --git a/tests/components/tuya/fixtures/dj_tgewj70aowigv8fz.json b/tests/components/tuya/fixtures/dj_tgewj70aowigv8fz.json new file mode 100644 index 00000000000..d02a94e0f71 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_tgewj70aowigv8fz.json @@ -0,0 +1,140 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Stairs", + "category": "dj", + "product_id": "tgewj70aowigv8fz", + "product_name": "RGBC Smart Bulb", + "online": true, + "sub": false, + "time_zone": "+00:00", + "active_time": "2023-04-28T07:01:03+00:00", + "create_time": "2023-04-28T07:01:03+00:00", + "update_time": "2023-04-28T07:01:03+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": [ + "white", + "colour", + "scene", + "scene_1", + "scene_2", + "scene_3", + "scene_4" + ] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": [ + "white", + "colour", + "scene", + "scene_1", + "scene_2", + "scene_3", + "scene_4" + ] + } + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + }, + "colour_data": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + } + }, + "status": { + "switch_led": false, + "work_mode": "colour", + "bright_value": 71, + "colour_data": { + "h": 0.0, + "s": 0.0, + "v": 255.0 + } + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json index 20c91ad7739..91c4dff5a42 100644 --- a/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json +++ b/tests/components/tuya/fixtures/dj_tmsloaroqavbucgn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf252b8ee16b2e78bdoxlp", "name": "Pokerlamp 1", "category": "dj", "product_id": "tmsloaroqavbucgn", diff --git a/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json index 7ea5905411d..4b7a3a4e879 100644 --- a/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json +++ b/tests/components/tuya/fixtures/dj_ufq2xwuzd4nb0qdr.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf8edbd51a52c01a4bfgqf", "name": "Sjiethoes", "category": "dj", "product_id": "ufq2xwuzd4nb0qdr", diff --git a/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json index 4d6749ea0b4..9aa3646a11b 100644 --- a/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json +++ b/tests/components/tuya/fixtures/dj_vqwcnabamzrc2kab.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfd56f4718874ee8830xdw", "name": "Strip 2", "category": "dj", "product_id": "vqwcnabamzrc2kab", diff --git a/tests/components/tuya/fixtures/dj_xdvitmhhmgefaeuq.json b/tests/components/tuya/fixtures/dj_xdvitmhhmgefaeuq.json new file mode 100644 index 00000000000..32688d06f5a --- /dev/null +++ b/tests/components/tuya/fixtures/dj_xdvitmhhmgefaeuq.json @@ -0,0 +1,510 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "druckerhell", + "category": "dj", + "product_id": "xdvitmhhmgefaeuq", + "product_name": "GU10 Smart Bulb", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-15T13:02:08+00:00", + "create_time": "2025-03-15T13:02:08+00:00", + "update_time": "2025-03-15T13:02:08+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 0, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json index cce66d90b0c..2e339c64678 100644 --- a/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json +++ b/tests/components/tuya/fixtures/dj_xokdfs6kh5ednakk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfc1ef4da4accc0731oggw", "name": "ERKER 1-Gold ", "category": "dj", "product_id": "xokdfs6kh5ednakk", diff --git a/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json index d1c23663144..2a6b4f34ce7 100644 --- a/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json +++ b/tests/components/tuya/fixtures/dj_zakhnlpdiu0ycdxn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "03010850c44f33966362", "name": "Stoel", "category": "dj", "product_id": "zakhnlpdiu0ycdxn", diff --git a/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json index 624f7fb4347..0ae793b3d1b 100644 --- a/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json +++ b/tests/components/tuya/fixtures/dj_zav1pa32pyxray78.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "500425642462ab50909b", "name": "Gengske 💡 ", "category": "dj", "product_id": "zav1pa32pyxray78", diff --git a/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json index cede2b65682..b500c67d0ea 100644 --- a/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json +++ b/tests/components/tuya/fixtures/dj_zputiamzanuk6yky.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf74164049de868395pbci", "name": "Floodlight", "category": "dj", "product_id": "zputiamzanuk6yky", diff --git a/tests/components/tuya/fixtures/dlq_cnpkf4xdmd9v49iq.json b/tests/components/tuya/fixtures/dlq_cnpkf4xdmd9v49iq.json new file mode 100644 index 00000000000..aa42fc0f568 --- /dev/null +++ b/tests/components/tuya/fixtures/dlq_cnpkf4xdmd9v49iq.json @@ -0,0 +1,158 @@ +{ + "endpoint": "https://apigw.tuyacn.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "\u65ad\u8def\u5668HA", + "category": "dlq", + "product_id": "cnpkf4xdmd9v49iq", + "product_name": "Breaker", + "online": true, + "sub": false, + "time_zone": "+08:00", + "active_time": "2025-07-03T10:19:11+00:00", + "create_time": "2025-07-03T10:19:11+00:00", + "update_time": "2025-07-03T10:19:11+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "alarm_set_1": { + "type": "Raw", + "value": {} + }, + "alarm_set_2": { + "type": "Raw", + "value": {} + }, + "leakagecurr_test": { + "type": "Boolean", + "value": {} + }, + "online_state": { + "type": "Enum", + "value": { + "range": ["online", "offline"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "forward_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 3, + "step": 1 + } + }, + "phase_a": { + "type": "Raw", + "value": {} + }, + "fault": { + "type": "Bitmap", + "value": { + "label": [ + "short_circuit_alarm", + "surge_alarm", + "overload_alarm", + "leakagecurr_alarm", + "temp_dif_fault", + "fire_alarm", + "high_power_alarm", + "self_test_alarm", + "ov_cr", + "unbalance_alarm", + "ov_vol", + "undervoltage_alarm", + "miss_phase_alarm", + "outage_alarm", + "magnetism_alarm", + "credit_alarm", + "no_balance_alarm", + "leakage_early_warning", + "overcur_early_warning", + "overvol_early_warning", + "overpow_early_warning", + "undvol_early_warning", + "higtemp_early_warning" + ] + } + }, + "leakage_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "alarm_set_1": { + "type": "Raw", + "value": {} + }, + "alarm_set_2": { + "type": "Raw", + "value": {} + }, + "breaker_number": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "leakagecurr_test": { + "type": "Boolean", + "value": {} + }, + "supply_frequency": { + "type": "Integer", + "value": { + "unit": "Hz", + "min": 0, + "max": 9999, + "scale": 2, + "step": 1 + } + }, + "online_state": { + "type": "Enum", + "value": { + "range": ["online", "offline"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "forward_energy_total": 120, + "phase_a": "Ag8JJQAASAAACAAAAAAACGME", + "fault": 0, + "leakage_current": 0, + "switch": false, + "alarm_set_1": "", + "alarm_set_2": "", + "breaker_number": "", + "leakagecurr_test": false, + "supply_frequency": 0, + "online_state": "online", + "child_lock": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dlq_jdj6ccklup7btq3a.json b/tests/components/tuya/fixtures/dlq_jdj6ccklup7btq3a.json new file mode 100644 index 00000000000..0b8bceb73e3 --- /dev/null +++ b/tests/components/tuya/fixtures/dlq_jdj6ccklup7btq3a.json @@ -0,0 +1,216 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Eau Chaude", + "category": "dlq", + "product_id": "jdj6ccklup7btq3a", + "product_name": "WiFi Din Rail Switch with metering", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-04-14T12:13:35+00:00", + "create_time": "2025-04-14T12:13:35+00:00", + "update_time": "2025-04-14T12:13:35+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none", "on"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "online_state": { + "type": "Enum", + "value": { + "range": ["online", "offline"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 9999999, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 100000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 999999, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "test_bit": { + "type": "Integer", + "value": { + "min": 0, + "max": 5, + "scale": 0, + "step": 1 + } + }, + "voltage_coe": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "electric_coe": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "power_coe": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "electricity_coe": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000000, + "scale": 0, + "step": 1 + } + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["ov_cr", "ov_vol", "ov_pwr", "ls_cr", "ls_vol", "ls_pow"] + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none", "on"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "online_state": { + "type": "Enum", + "value": { + "range": ["online", "offline"] + } + } + }, + "status": { + "switch": true, + "countdown_1": 0, + "add_ele": 390, + "cur_current": 10067, + "cur_power": 24417, + "cur_voltage": 2419, + "test_bit": 1, + "voltage_coe": 15943, + "electric_coe": 12577, + "power_coe": 3125, + "electricity_coe": 2682, + "fault": 0, + "relay_status": "last", + "light_mode": "relay", + "child_lock": false, + "cycle_time": "", + "online_state": "online" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json b/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json index 2652399bdcb..eaec5aed56c 100644 --- a/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json +++ b/tests/components/tuya/fixtures/dlq_kxdr6su0c55p7bbo.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": null, "disabled_by": null, "disabled_polling": false, - "id": "bf5e5bde2c52cb5994cd27", "name": "Metering_3PN_WiFi_stable", "category": "dlq", "product_id": "kxdr6su0c55p7bbo", diff --git a/tests/components/tuya/fixtures/dlq_r9kg2g1uhhyicycb.json b/tests/components/tuya/fixtures/dlq_r9kg2g1uhhyicycb.json new file mode 100644 index 00000000000..3ebbb27b349 --- /dev/null +++ b/tests/components/tuya/fixtures/dlq_r9kg2g1uhhyicycb.json @@ -0,0 +1,143 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": false, + "disabled_by": null, + "disabled_polling": false, + "name": "P1 Energia Elettrica", + "category": "dlq", + "product_id": "r9kg2g1uhhyicycb", + "product_name": "Breaker ", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2022-12-10T17:33:13+00:00", + "create_time": "2022-12-10T17:33:13+00:00", + "update_time": "2022-12-10T17:33:13+00:00", + "function": { + "switch_prepayment": { + "type": "Boolean", + "value": {} + }, + "clear_energy": { + "type": "Boolean", + "value": {} + }, + "charge_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 999999, + "scale": 2, + "step": 1 + } + }, + "switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "total_forward_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "phase_a": { + "type": "Raw", + "value": {} + }, + "phase_b": { + "type": "Raw", + "value": {} + }, + "phase_c": { + "type": "Raw", + "value": {} + }, + "fault": { + "type": "Bitmap", + "value": { + "label": [ + "short_circuit_alarm", + "surge_alarm", + "overload_alarm", + "leakagecurr_alarm", + "temp_dif_fault", + "fire_alarm", + "high_power_alarm", + "self_test_alarm", + "ov_cr", + "unbalance_alarm", + "ov_vol", + "undervoltage_alarm", + "miss_phase_alarm", + "outage_alarm", + "magnetism_alarm", + "credit_alarm", + "no_balance_alarm" + ] + } + }, + "switch_prepayment": { + "type": "Boolean", + "value": {} + }, + "energy_reset": { + "type": "Enum", + "value": { + "range": ["empty"] + } + }, + "balance_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "charge_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 999999, + "scale": 2, + "step": 1 + } + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "breaker_number": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status": { + "total_forward_energy": 2279960, + "phase_a": "CGYAPCgADPIACw==", + "phase_b": "AAAAAAAAAAAAAA==", + "phase_c": "AAAAAAAAAAAAAA==", + "fault": 0, + "switch_prepayment": false, + "energy_reset": "", + "balance_energy": 0, + "charge_energy": 0, + "switch": true, + "breaker_number": "FSE-F723C5EA0AC8B6" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dlq_z3jngbyubvwgfrcv.json b/tests/components/tuya/fixtures/dlq_z3jngbyubvwgfrcv.json new file mode 100644 index 00000000000..695b8a35414 --- /dev/null +++ b/tests/components/tuya/fixtures/dlq_z3jngbyubvwgfrcv.json @@ -0,0 +1,222 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Edesanya Energy", + "category": "dlq", + "product_id": "z3jngbyubvwgfrcv", + "product_name": "Breaker", + "online": true, + "sub": false, + "time_zone": "+03:00", + "active_time": "2025-06-16T11:30:16+00:00", + "create_time": "2025-06-16T11:30:16+00:00", + "update_time": "2025-06-16T11:30:16+00:00", + "function": { + "switch_prepayment": { + "type": "Boolean", + "value": {} + }, + "clear_energy": { + "type": "Boolean", + "value": {} + }, + "charge_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 999999, + "scale": 2, + "step": 1 + } + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "alarm_set_1": { + "type": "Raw", + "value": {} + }, + "alarm_set_2": { + "type": "Raw", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "cycle_time": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "random_time": { + "type": "String", + "value": {} + } + }, + "status_range": { + "total_forward_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "phase_a": { + "type": "Raw", + "value": {} + }, + "phase_b": { + "type": "Raw", + "value": {} + }, + "phase_c": { + "type": "Raw", + "value": {} + }, + "fault": { + "type": "Bitmap", + "value": { + "label": [ + "short_circuit_alarm", + "surge_alarm", + "overload_alarm", + "leakagecurr_alarm", + "temp_dif_fault", + "fire_alarm", + "high_power_alarm", + "self_test_alarm", + "ov_cr", + "unbalance_alarm", + "ov_vol", + "undervoltage_alarm", + "miss_phase_alarm", + "outage_alarm", + "magnetism_alarm", + "credit_alarm", + "no_balance_alarm", + "phase_seq_err_alarm", + "vol_unbalance_alarm", + "low_current_alarm" + ] + } + }, + "switch_prepayment": { + "type": "Boolean", + "value": {} + }, + "energy_reset": { + "type": "Enum", + "value": { + "range": ["empty"] + } + }, + "balance_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "charge_energy": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 999999, + "scale": 2, + "step": 1 + } + }, + "leakage_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "alarm_set_1": { + "type": "Raw", + "value": {} + }, + "alarm_set_2": { + "type": "Raw", + "value": {} + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -20, + "max": 200, + "scale": 0, + "step": 1 + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "cycle_time": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "random_time": { + "type": "String", + "value": {} + } + }, + "status": { + "total_forward_energy": 21972, + "phase_a": "CT0AAmAAAIU=", + "phase_b": "", + "phase_c": "", + "fault": 0, + "switch_prepayment": false, + "energy_reset": "", + "balance_energy": 0, + "charge_energy": 0, + "leakage_current": 0, + "switch": true, + "alarm_set_1": "BAEAMgUBAFA=", + "alarm_set_2": "AQECdgMBARMEAQCv", + "temp_current": 24, + "countdown_1": 0, + "cycle_time": "EwAAAAAAAAAAAA==", + "random_time": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dr_pjvxl1wsyqxivsaf.json b/tests/components/tuya/fixtures/dr_pjvxl1wsyqxivsaf.json new file mode 100644 index 00000000000..78fce362d37 --- /dev/null +++ b/tests/components/tuya/fixtures/dr_pjvxl1wsyqxivsaf.json @@ -0,0 +1,185 @@ +{ + "endpoint": "https://openapi.tuyaus.com", + "auth_type": 0, + "country_code": "1", + "app_type": "smartlife", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Sunbeam Bedding", + "model": "", + "category": "dr", + "product_id": "pjvxl1wsyqxivsaf", + "product_name": "Sunbeam Bedding", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2022-11-07T00:20:52+00:00", + "create_time": "2022-11-01T00:43:45+00:00", + "update_time": "2022-11-07T00:20:52+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "level": { + "type": "Enum", + "value": { + "range": [ + "level_1", + "level_2", + "level_3", + "level_4", + "level_5", + "level_6", + "level_7", + "level_8", + "level_9", + "level_10" + ] + } + }, + "preheat": { + "type": "Boolean", + "value": {} + }, + "preheat_1": { + "type": "Boolean", + "value": {} + }, + "switch_1": { + "type": "Boolean", + "value": {} + }, + "preheat_2": { + "type": "Boolean", + "value": {} + }, + "level_1": { + "type": "Enum", + "value": { + "range": [ + "level_1", + "level_2", + "level_3", + "level_4", + "level_5", + "level_6", + "level_7", + "level_8", + "level_9", + "level_10" + ] + } + }, + "level_2": { + "type": "Enum", + "value": { + "range": [ + "level_1", + "level_2", + "level_3", + "level_4", + "level_5", + "level_6", + "level_7", + "level_8", + "level_9", + "level_10" + ] + } + } + }, + "status_range": { + "level_1": { + "type": "Enum", + "value": { + "range": [ + "level_1", + "level_2", + "level_3", + "level_4", + "level_5", + "level_6", + "level_7", + "level_8", + "level_9", + "level_10" + ] + } + }, + "level_2": { + "type": "Enum", + "value": { + "range": [ + "level_1", + "level_2", + "level_3", + "level_4", + "level_5", + "level_6", + "level_7", + "level_8", + "level_9", + "level_10" + ] + } + }, + "level": { + "type": "Enum", + "value": { + "range": [ + "level_1", + "level_2", + "level_3", + "level_4", + "level_5", + "level_6", + "level_7", + "level_8", + "level_9", + "level_10" + ] + } + }, + "preheat": { + "type": "Boolean", + "value": {} + }, + "preheat_2": { + "type": "Boolean", + "value": {} + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "preheat_1": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch": false, + "level": "level_5", + "preheat": false, + "switch_1": false, + "switch_2": false, + "level_1": "level_5", + "level_2": "level_5", + "preheat_1": false, + "preheat_2": false + } +} diff --git a/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json b/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json index 3aae03c904a..9a82643e2f9 100644 --- a/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json +++ b/tests/components/tuya/fixtures/fs_g0ewlb1vmwqljzji.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "XXX", "name": "Ceiling Fan With Light", "category": "fs", "product_id": "g0ewlb1vmwqljzji", diff --git a/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json b/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json index 02b3808f84d..e8c59f50d7f 100644 --- a/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json +++ b/tests/components/tuya/fixtures/fs_ibytpo6fpnugft1c.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "10706550a4e57c88b93a", "name": "Ventilador Cama", "category": "fs", "product_id": "ibytpo6fpnugft1c", diff --git a/tests/components/tuya/fixtures/fsd_9ecs16c53uqskxw6.json b/tests/components/tuya/fixtures/fsd_9ecs16c53uqskxw6.json new file mode 100644 index 00000000000..92c83e9e8f0 --- /dev/null +++ b/tests/components/tuya/fixtures/fsd_9ecs16c53uqskxw6.json @@ -0,0 +1,151 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "ceiling fan/Light v2", + "category": "fsd", + "product_id": "9ecs16c53uqskxw6", + "product_name": "ceiling fan/Light v2", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-07-08T17:05:22+00:00", + "create_time": "2025-07-08T17:05:22+00:00", + "update_time": "2025-07-08T17:05:22+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "fan_switch": { + "type": "Boolean", + "value": {} + }, + "fan_speed": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 6, + "scale": 0, + "step": 1 + } + }, + "fan_direction": { + "type": "Enum", + "value": { + "range": ["forward", "reverse"] + } + }, + "countdown_left_fan": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 540, + "scale": 0, + "step": 1 + } + }, + "fan_beep": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "fan_switch": { + "type": "Boolean", + "value": {} + }, + "fan_speed": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 6, + "scale": 0, + "step": 1 + } + }, + "fan_direction": { + "type": "Enum", + "value": { + "range": ["forward", "reverse"] + } + }, + "countdown_left_fan": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 540, + "scale": 0, + "step": 1 + } + }, + "fan_beep": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "temp_value": 0, + "scene_data": "", + "fan_switch": true, + "fan_speed": 2, + "fan_direction": "forward", + "countdown_left_fan": 0, + "fan_beep": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json b/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json index ddfbce3ae11..62723670973 100644 --- a/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json +++ b/tests/components/tuya/fixtures/gyd_lgekqfxdabipm3tn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "eb3e988f33c233290cfs3l", "name": "Colorful PIR Night Light", "category": "gyd", "product_id": "lgekqfxdabipm3tn", diff --git a/tests/components/tuya/fixtures/hps_2aaelwxk.json b/tests/components/tuya/fixtures/hps_2aaelwxk.json index 4e5066e77f4..77c4ad47839 100644 --- a/tests/components/tuya/fixtures/hps_2aaelwxk.json +++ b/tests/components/tuya/fixtures/hps_2aaelwxk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf78687ad321a3aeb8a73m", "name": "Human presence Office", "category": "hps", "product_id": "2aaelwxk", diff --git a/tests/components/tuya/fixtures/hps_wqashyqo.json b/tests/components/tuya/fixtures/hps_wqashyqo.json new file mode 100644 index 00000000000..ad784b34aa9 --- /dev/null +++ b/tests/components/tuya/fixtures/hps_wqashyqo.json @@ -0,0 +1,41 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Soil moisture sensor #1", + "category": "hps", + "product_id": "wqashyqo", + "product_name": "Soil moisture sensor", + "online": true, + "sub": true, + "time_zone": "+01:00", + "active_time": "2025-06-03T10:48:45+00:00", + "create_time": "2025-06-03T10:48:45+00:00", + "update_time": "2025-06-03T10:48:45+00:00", + "function": {}, + "status_range": { + "presence_state": { + "type": "Enum", + "value": { + "range": ["none"] + } + }, + "humidity_value": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "presence_state": "none", + "humidity_value": 59 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/hwsb_ircs2n82vgrozoew.json b/tests/components/tuya/fixtures/hwsb_ircs2n82vgrozoew.json new file mode 100644 index 00000000000..228f4848d5e --- /dev/null +++ b/tests/components/tuya/fixtures/hwsb_ircs2n82vgrozoew.json @@ -0,0 +1,34 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "InverFlow", + "category": "hwsb", + "product_id": "ircs2n82vgrozoew", + "product_name": "InverFlow", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-08-08T12:44:43+00:00", + "create_time": "2025-08-08T12:44:43+00:00", + "update_time": "2025-08-08T12:44:43+00:00", + "function": {}, + "status_range": { + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 3000, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "cur_power": 405 + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/jtmspro_xqeob8h6.json b/tests/components/tuya/fixtures/jtmspro_xqeob8h6.json new file mode 100644 index 00000000000..e18b537c7e6 --- /dev/null +++ b/tests/components/tuya/fixtures/jtmspro_xqeob8h6.json @@ -0,0 +1,398 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "S1-TY-BLE-PRO 2", + "category": "jtmspro", + "product_id": "xqeob8h6", + "product_name": "S1-TY-BLE-PRO", + "online": false, + "sub": true, + "time_zone": "+03:00", + "active_time": "2025-07-01T15:21:36+00:00", + "create_time": "2025-07-01T15:21:36+00:00", + "update_time": "2025-07-01T15:21:36+00:00", + "function": { + "unlock_method_create": { + "type": "Raw", + "value": {} + }, + "unlock_method_delete": { + "type": "Raw", + "value": {} + }, + "unlock_method_modify": { + "type": "Raw", + "value": {} + }, + "lock_record": { + "type": "Raw", + "value": {} + }, + "message": { + "type": "Boolean", + "value": {} + }, + "automatic_lock": { + "type": "Boolean", + "value": {} + }, + "unlock_switch": { + "type": "Enum", + "value": { + "range": ["single_unlock", "finger_card"] + } + }, + "auto_lock_time": { + "type": "Integer", + "value": { + "min": 1, + "max": 1800, + "scale": 0, + "step": 1 + } + }, + "rtc_lock": { + "type": "Boolean", + "value": {} + }, + "manual_lock": { + "type": "Boolean", + "value": {} + }, + "synch_method": { + "type": "Raw", + "value": {} + }, + "remote_no_dp_key": { + "type": "Raw", + "value": {} + }, + "record": { + "type": "Raw", + "value": {} + }, + "check_code_set": { + "type": "Raw", + "value": {} + }, + "ble_unlock_check": { + "type": "Raw", + "value": {} + }, + "remote_pd_setkey_check": { + "type": "Raw", + "value": {} + }, + "unlock_ble_ibeacon": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "ibeacon_scan_mode": { + "type": "Enum", + "value": { + "range": [ + "always", + "5min", + "10min", + "20min", + "40min", + "60min", + "90min", + "120min" + ] + } + }, + "rssi_sensitivity_level": { + "type": "Enum", + "value": { + "range": [ + "inactive", + "90db", + "80db", + "70db", + "60db", + "50db", + "40db", + "30db", + "20db" + ] + } + }, + "ibeacon_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "unlock_method_create": { + "type": "Raw", + "value": {} + }, + "unlock_method_delete": { + "type": "Raw", + "value": {} + }, + "unlock_method_modify": { + "type": "Raw", + "value": {} + }, + "residual_electricity": { + "type": "Integer", + "value": { + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "unlock_fingerprint": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_card": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_key": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_ble": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "lock_record": { + "type": "Raw", + "value": {} + }, + "alarm_lock": { + "type": "Enum", + "value": { + "range": [ + "wrong_finger", + "wrong_password", + "wrong_card", + "wrong_face", + "tongue_bad", + "too_hot", + "unclosed_time", + "tongue_not_out", + "pry", + "key_in", + "low_battery", + "power_off", + "shock", + "defense" + ] + } + }, + "hijack": { + "type": "Boolean", + "value": {} + }, + "doorbell": { + "type": "Boolean", + "value": {} + }, + "message": { + "type": "Boolean", + "value": {} + }, + "automatic_lock": { + "type": "Boolean", + "value": {} + }, + "unlock_switch": { + "type": "Enum", + "value": { + "range": ["single_unlock", "finger_card"] + } + }, + "auto_lock_time": { + "type": "Integer", + "value": { + "min": 1, + "max": 1800, + "scale": 0, + "step": 1 + } + }, + "closed_opened": { + "type": "Enum", + "value": { + "range": ["unknown", "open", "closed"] + } + }, + "rtc_lock": { + "type": "Boolean", + "value": {} + }, + "manual_lock": { + "type": "Boolean", + "value": {} + }, + "lock_motor_state": { + "type": "Boolean", + "value": {} + }, + "synch_method": { + "type": "Raw", + "value": {} + }, + "remote_no_dp_key": { + "type": "Raw", + "value": {} + }, + "unlock_phone_remote": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_voice_remote": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "record": { + "type": "Raw", + "value": {} + }, + "check_code_set": { + "type": "Raw", + "value": {} + }, + "ble_unlock_check": { + "type": "Raw", + "value": {} + }, + "unlock_record_check": { + "type": "Raw", + "value": {} + }, + "remote_pd_setkey_check": { + "type": "Raw", + "value": {} + }, + "unlock_double_kit": { + "type": "Raw", + "value": {} + }, + "unlock_ble_ibeacon": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "ibeacon_scan_mode": { + "type": "Enum", + "value": { + "range": [ + "always", + "5min", + "10min", + "20min", + "40min", + "60min", + "90min", + "120min" + ] + } + }, + "rssi_sensitivity_level": { + "type": "Enum", + "value": { + "range": [ + "inactive", + "90db", + "80db", + "70db", + "60db", + "50db", + "40db", + "30db", + "20db" + ] + } + }, + "ibeacon_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "unlock_method_create": "A/8BAQIAAA==", + "unlock_method_delete": "", + "unlock_method_modify": "", + "residual_electricity": 99, + "unlock_fingerprint": 1, + "unlock_card": 0, + "unlock_key": 0, + "unlock_ble": 1, + "lock_record": "", + "alarm_lock": "wrong_finger", + "hijack": false, + "doorbell": false, + "message": false, + "automatic_lock": true, + "unlock_switch": "single_unlock", + "auto_lock_time": 3, + "closed_opened": "unknown", + "rtc_lock": false, + "manual_lock": true, + "lock_motor_state": false, + "synch_method": "AQA=", + "remote_no_dp_key": "AAAB", + "unlock_phone_remote": 1, + "unlock_voice_remote": 0, + "record": "AAEB", + "check_code_set": "AAH//wAAAAAAAAAAAP//AA==", + "ble_unlock_check": "AAH//zY2MDkzNTA0AWhkA/QAAA==", + "unlock_record_check": "", + "remote_pd_setkey_check": "AAH//zY2MDkzNTA0AQABAA==", + "unlock_double_kit": "", + "unlock_ble_ibeacon": 0, + "ibeacon_scan_mode": "always", + "rssi_sensitivity_level": "inactive", + "ibeacon_switch": false + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kg_4nqs33emdwJxpQ8O.json b/tests/components/tuya/fixtures/kg_4nqs33emdwJxpQ8O.json new file mode 100644 index 00000000000..bb6f8a8bba8 --- /dev/null +++ b/tests/components/tuya/fixtures/kg_4nqs33emdwJxpQ8O.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "office lights", + "category": "kg", + "product_id": "4nqs33emdwJxpQ8O", + "product_name": "SWITCH1", + "online": true, + "sub": false, + "time_zone": "+00:00", + "active_time": "2023-04-28T06:09:14+00:00", + "create_time": "2023-04-28T06:09:14+00:00", + "update_time": "2023-04-28T06:09:14+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kg_5ftkaulg.json b/tests/components/tuya/fixtures/kg_5ftkaulg.json new file mode 100644 index 00000000000..4b629f86375 --- /dev/null +++ b/tests/components/tuya/fixtures/kg_5ftkaulg.json @@ -0,0 +1,80 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "bathroom light", + "category": "kg", + "product_id": "5ftkaulg", + "product_name": "1Gang Zigbee Switch", + "online": true, + "sub": true, + "time_zone": "+00:00", + "active_time": "2023-07-04T15:11:35+00:00", + "create_time": "2023-07-04T15:11:35+00:00", + "update_time": "2023-07-04T15:11:35+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["none", "relay", "pos"] + } + } + }, + "status": { + "switch_1": false, + "countdown_1": 0, + "relay_status": "power_off", + "light_mode": "pos" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json b/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json index a190161953b..a61ebc52659 100644 --- a/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json +++ b/tests/components/tuya/fixtures/kg_gbm9ata1zrzaez4a.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": true, - "id": "0665305284f3ebe9fdc1", "name": "QT-Switch", "category": "kg", "product_id": "gbm9ata1zrzaez4a", diff --git a/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json b/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json index 5758fce2152..4e148140624 100644 --- a/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json +++ b/tests/components/tuya/fixtures/kj_CAjWAxBUZt7QZHfz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "152027113c6105cce49c", "name": "HL400", "category": "kj", "product_id": "CAjWAxBUZt7QZHfz", diff --git a/tests/components/tuya/fixtures/kj_fsxtzzhujkrak2oy.json b/tests/components/tuya/fixtures/kj_fsxtzzhujkrak2oy.json new file mode 100644 index 00000000000..1fe8ead167f --- /dev/null +++ b/tests/components/tuya/fixtures/kj_fsxtzzhujkrak2oy.json @@ -0,0 +1,104 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Kalado Air Purifier", + "category": "kj", + "product_id": "fsxtzzhujkrak2oy", + "product_name": "", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-06-23T16:03:21+00:00", + "create_time": "2024-06-23T16:03:21+00:00", + "update_time": "2024-06-23T16:03:21+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto", "sleep"] + } + }, + "filter_reset": { + "type": "Boolean", + "value": {} + }, + "countdown_set": { + "type": "Enum", + "value": { + "range": ["cancel", "1h", "2h", "3h", "4h", "5h"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "pm25": { + "type": "Integer", + "value": { + "unit": "ug/m3", + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["manual", "auto", "sleep"] + } + }, + "filter": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "filter_reset": { + "type": "Boolean", + "value": {} + }, + "countdown_set": { + "type": "Enum", + "value": { + "range": ["cancel", "1h", "2h", "3h", "4h", "5h"] + } + }, + "air_quality": { + "type": "Enum", + "value": { + "range": ["great", "good", "medium", "severe"] + } + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["e1", "e2"] + } + } + }, + "status": { + "switch": false, + "pm25": 3, + "mode": "auto", + "filter": 42, + "filter_reset": false, + "countdown_set": "cancel", + "air_quality": "great", + "fault": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kj_s4uzibibgzdxzowo.json b/tests/components/tuya/fixtures/kj_s4uzibibgzdxzowo.json new file mode 100644 index 00000000000..b4ae9a35391 --- /dev/null +++ b/tests/components/tuya/fixtures/kj_s4uzibibgzdxzowo.json @@ -0,0 +1,115 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "ION1000PRO", + "category": "kj", + "product_id": "s4uzibibgzdxzowo", + "product_name": "", + "online": true, + "sub": false, + "time_zone": "+08:00", + "active_time": "2023-12-21T05:50:50+00:00", + "create_time": "2023-12-21T05:50:50+00:00", + "update_time": "2023-12-21T05:50:50+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "anion": { + "type": "Boolean", + "value": {} + }, + "lock": { + "type": "Boolean", + "value": {} + }, + "uv": { + "type": "Boolean", + "value": {} + }, + "filter_reset": { + "type": "Boolean", + "value": {} + }, + "countdown": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "pm25": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "anion": { + "type": "Boolean", + "value": {} + }, + "lock": { + "type": "Boolean", + "value": {} + }, + "uv": { + "type": "Boolean", + "value": {} + }, + "filter_reset": { + "type": "Boolean", + "value": {} + }, + "filter_days": { + "type": "Integer", + "value": { + "unit": "Hours", + "min": 0, + "max": 3600, + "scale": 0, + "step": 1 + } + }, + "countdown_set": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6"] + } + }, + "countdown_left": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 720, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": true, + "pm25": 9, + "anion": false, + "lock": true, + "uv": true, + "filter_reset": false, + "filter_days": 0, + "countdown_set": "cancle", + "countdown_left": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json b/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json index 642ef968608..45015bff0ac 100644 --- a/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json +++ b/tests/components/tuya/fixtures/kj_yrzylxax1qspdgpp.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "CENSORED", "name": "Bree", "category": "kj", "product_id": "yrzylxax1qspdgpp", diff --git a/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json b/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json index cb158a967b4..b36064724af 100644 --- a/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json +++ b/tests/components/tuya/fixtures/ks_j9fa8ahzac8uvlfl.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Tower Fan CA-407G Smart", "category": "ks", "product_id": "j9fa8ahzac8uvlfl", diff --git a/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json b/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json index 5b29fd0a191..3dd9c3713dc 100644 --- a/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json +++ b/tests/components/tuya/fixtures/kt_5wnlzekkstwcdsvm.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "Air Conditioner", "category": "kt", "product_id": "5wnlzekkstwcdsvm", diff --git a/tests/components/tuya/fixtures/kt_ibmmirhhq62mmf1g.json b/tests/components/tuya/fixtures/kt_ibmmirhhq62mmf1g.json new file mode 100644 index 00000000000..e7657a7b0e9 --- /dev/null +++ b/tests/components/tuya/fixtures/kt_ibmmirhhq62mmf1g.json @@ -0,0 +1,110 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Master Bedroom AC", + "category": "kt", + "product_id": "ibmmirhhq62mmf1g", + "product_name": "T platform model-USB ", + "online": true, + "sub": false, + "time_zone": "-07:00", + "active_time": "2025-07-16T14:12:18+00:00", + "create_time": "2025-07-16T14:12:18+00:00", + "update_time": "2025-07-16T14:12:18+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 160, + "max": 880, + "scale": 1, + "step": 5 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["cold", "hot", "wet", "wind", "auto"] + } + }, + "temp_set_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 61, + "max": 88, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 160, + "max": 880, + "scale": 1, + "step": 5 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -20, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["cold", "hot", "wet", "wind", "auto"] + } + }, + "humidity_current": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_set_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 61, + "max": 88, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": true, + "temp_set": 750, + "temp_current": 26, + "mode": "cold", + "humidity_current": 0, + "temp_set_f": 61 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/kt_vdadlnmsorlhw4td.json b/tests/components/tuya/fixtures/kt_vdadlnmsorlhw4td.json new file mode 100644 index 00000000000..0f07e4a13e7 --- /dev/null +++ b/tests/components/tuya/fixtures/kt_vdadlnmsorlhw4td.json @@ -0,0 +1,78 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Sove", + "category": "kt", + "product_id": "vdadlnmsorlhw4td", + "product_name": "YFA-05C", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-07-07T11:44:00+00:00", + "create_time": "2025-07-07T11:44:00+00:00", + "update_time": "2025-07-07T11:44:00+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 16, + "max": 86, + "scale": 0, + "step": 1 + } + }, + "windspeed": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 16, + "max": 86, + "scale": 0, + "step": 1 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -7, + "max": 110, + "scale": 0, + "step": 1 + } + }, + "windspeed": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + } + }, + "status": { + "switch": false, + "temp_set": 16, + "temp_current": 24, + "windspeed": 2 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ldcg_9kbbfeho.json b/tests/components/tuya/fixtures/ldcg_9kbbfeho.json new file mode 100644 index 00000000000..6281085a06c --- /dev/null +++ b/tests/components/tuya/fixtures/ldcg_9kbbfeho.json @@ -0,0 +1,45 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Luminosité", + "category": "ldcg", + "product_id": "9kbbfeho", + "product_name": "Luminance sensor", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-25T16:37:21+00:00", + "create_time": "2025-07-25T16:37:21+00:00", + "update_time": "2025-07-25T16:37:21+00:00", + "function": {}, + "status_range": { + "bright_value": { + "type": "Integer", + "value": { + "unit": "lux", + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "bright_value": 16, + "battery_percentage": 91 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json b/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json index 1a25a84ec2c..ee69a811a92 100644 --- a/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json +++ b/tests/components/tuya/fixtures/mal_gyitctrjj1kefxp2.json @@ -1,5 +1,4 @@ { - "id": "123123aba12312312dazub", "name": "Multifunction alarm", "category": "mal", "product_id": "gyitctrjj1kefxp2", diff --git a/tests/components/tuya/fixtures/mc_oSQljE9YDqwCwTUA.json b/tests/components/tuya/fixtures/mc_oSQljE9YDqwCwTUA.json new file mode 100644 index 00000000000..16d51063dc1 --- /dev/null +++ b/tests/components/tuya/fixtures/mc_oSQljE9YDqwCwTUA.json @@ -0,0 +1,35 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Kippenluik", + "category": "mc", + "product_id": "oSQljE9YDqwCwTUA", + "product_name": "Door Sensor", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-10-28T09:22:24+00:00", + "create_time": "2023-10-28T09:22:24+00:00", + "update_time": "2023-10-28T09:22:24+00:00", + "function": {}, + "status_range": { + "doorcontact_state": { + "type": "Boolean", + "value": {} + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "doorcontact_state": true, + "battery_state": "high" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mcs_6ywsnauy.json b/tests/components/tuya/fixtures/mcs_6ywsnauy.json new file mode 100644 index 00000000000..8612a42c0c6 --- /dev/null +++ b/tests/components/tuya/fixtures/mcs_6ywsnauy.json @@ -0,0 +1,44 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Fen\u00eatre cuisine", + "category": "mcs", + "product_id": "6ywsnauy", + "product_name": "Contact Sensor", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T11:54:49+00:00", + "create_time": "2025-07-19T11:54:49+00:00", + "update_time": "2025-07-19T11:54:49+00:00", + "function": {}, + "status_range": { + "doorcontact_state": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temper_alarm": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "doorcontact_state": false, + "battery_percentage": 93, + "temper_alarm": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json b/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json index c73b6c34878..0e0a947aff7 100644 --- a/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json +++ b/tests/components/tuya/fixtures/mcs_7jIGJAymiH8OsFFb.json @@ -6,7 +6,6 @@ "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf5cccf9027080e2dbb9w3", "name": "Door Garage ", "model": "", "category": "mcs", diff --git a/tests/components/tuya/fixtures/mcs_8yhypbo7.json b/tests/components/tuya/fixtures/mcs_8yhypbo7.json new file mode 100644 index 00000000000..ee5e125acd5 --- /dev/null +++ b/tests/components/tuya/fixtures/mcs_8yhypbo7.json @@ -0,0 +1,39 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Bo\u00eete aux lettres - arri\u00e8re", + "category": "mcs", + "product_id": "8yhypbo7", + "product_name": "Door Sensor", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T11:44:37+00:00", + "create_time": "2025-07-19T11:44:37+00:00", + "update_time": "2025-07-19T11:44:37+00:00", + "function": {}, + "status_range": { + "doorcontact_state": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "doorcontact_state": false, + "battery_percentage": 62 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mcs_hx5ztlztij4yxxvg.json b/tests/components/tuya/fixtures/mcs_hx5ztlztij4yxxvg.json new file mode 100644 index 00000000000..b0011708edf --- /dev/null +++ b/tests/components/tuya/fixtures/mcs_hx5ztlztij4yxxvg.json @@ -0,0 +1,35 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": true, + "name": "Steel cage door", + "category": "mcs", + "product_id": "hx5ztlztij4yxxvg", + "product_name": "Door Detector", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2020-05-28T22:07:06+00:00", + "create_time": "2020-05-28T22:07:06+00:00", + "update_time": "2020-05-28T22:07:06+00:00", + "function": {}, + "status_range": { + "doorcontact_state": { + "type": "Boolean", + "value": {} + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "doorcontact_state": false, + "battery_state": "high" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mcs_qxu3flpqjsc1kqu3.json b/tests/components/tuya/fixtures/mcs_qxu3flpqjsc1kqu3.json new file mode 100644 index 00000000000..708de40152f --- /dev/null +++ b/tests/components/tuya/fixtures/mcs_qxu3flpqjsc1kqu3.json @@ -0,0 +1,39 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Garage Contact Sensor", + "category": "mcs", + "product_id": "qxu3flpqjsc1kqu3", + "product_name": "Contact Sensor", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-03-11T23:28:59+00:00", + "create_time": "2023-03-11T23:28:59+00:00", + "update_time": "2023-03-11T23:28:59+00:00", + "function": {}, + "status_range": { + "doorcontact_state": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "doorcontact_state": false, + "battery_percentage": 11 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mzj_jlapoy5liocmtdvd.json b/tests/components/tuya/fixtures/mzj_jlapoy5liocmtdvd.json new file mode 100644 index 00000000000..804004a6d26 --- /dev/null +++ b/tests/components/tuya/fixtures/mzj_jlapoy5liocmtdvd.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "ISV-100W2.0", + "category": "mzj", + "product_id": "jlapoy5liocmtdvd", + "product_name": "ISV-100W2.0", + "online": true, + "sub": false, + "time_zone": "-04:00", + "active_time": "2025-08-14T21:06:41+00:00", + "create_time": "2025-08-14T21:06:41+00:00", + "update_time": "2025-08-14T21:06:41+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json b/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json index 402e73c732b..df6375a6827 100644 --- a/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json +++ b/tests/components/tuya/fixtures/mzj_qavcakohisj5adyh.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bff434eca843ffc9afmthv", "name": "Sous Vide", "category": "mzj", "product_id": "qavcakohisj5adyh", diff --git a/tests/components/tuya/fixtures/ntq_9mqdhwklpvnnvb7t.json b/tests/components/tuya/fixtures/ntq_9mqdhwklpvnnvb7t.json new file mode 100644 index 00000000000..0bc304817b4 --- /dev/null +++ b/tests/components/tuya/fixtures/ntq_9mqdhwklpvnnvb7t.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "\u0411\u0440\u0438\u0437\u0435\u0440 \u0417\u0430\u043b", + "category": "ntq", + "product_id": "9mqdhwklpvnnvb7t", + "product_name": "TION Breezer Bio X", + "online": true, + "sub": false, + "time_zone": "+03:00", + "active_time": "2024-08-16T14:49:45+00:00", + "create_time": "2024-08-16T14:49:45+00:00", + "update_time": "2024-08-16T14:49:45+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json index 4ed7ecf0373..aa16d5a91d8 100644 --- a/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json +++ b/tests/components/tuya/fixtures/pc_t2afic7i3v1bwhfp.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf2206da15147500969d6e", "name": "Bubbelbad", "category": "pc", "product_id": "t2afic7i3v1bwhfp", diff --git a/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json index 99929616ec7..ddff6df21a1 100644 --- a/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json +++ b/tests/components/tuya/fixtures/pc_trjopo1vdlt9q1tg.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "15727703c4dd5709cd78", "name": "Terras", "category": "pc", "product_id": "trjopo1vdlt9q1tg", diff --git a/tests/components/tuya/fixtures/pc_tsbguim4trl6fa7g.json b/tests/components/tuya/fixtures/pc_tsbguim4trl6fa7g.json new file mode 100644 index 00000000000..045b6383f72 --- /dev/null +++ b/tests/components/tuya/fixtures/pc_tsbguim4trl6fa7g.json @@ -0,0 +1,188 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Keller", + "category": "pc", + "product_id": "tsbguim4trl6fa7g", + "product_name": "Smart Power Strip", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-18T14:24:59+00:00", + "create_time": "2025-03-18T14:24:59+00:00", + "update_time": "2025-03-18T14:24:59+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "switch_3": { + "type": "Boolean", + "value": {} + }, + "switch_usb1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_3": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_usb1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "switch_3": { + "type": "Boolean", + "value": {} + }, + "switch_usb1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_3": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_usb1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 50000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "switch_1": false, + "switch_2": true, + "switch_3": false, + "switch_usb1": false, + "countdown_1": 0, + "countdown_2": 0, + "countdown_3": 0, + "countdown_usb1": 0, + "add_ele": 2, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pc_yku9wsimasckdt15.json b/tests/components/tuya/fixtures/pc_yku9wsimasckdt15.json new file mode 100644 index 00000000000..2d843bdf058 --- /dev/null +++ b/tests/components/tuya/fixtures/pc_yku9wsimasckdt15.json @@ -0,0 +1,189 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Framboisier", + "category": "pc", + "product_id": "yku9wsimasckdt15", + "product_name": "Konyks Priska USB", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-07-19T09:45:39+00:00", + "create_time": "2025-07-19T09:45:39+00:00", + "update_time": "2025-07-19T09:45:39+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "add_ele": { + "type": "Integer", + "value": { + "min": 0, + "max": 50000, + "scale": 3, + "step": 100 + } + }, + "cur_current": { + "type": "Integer", + "value": { + "unit": "mA", + "min": 0, + "max": 30000, + "scale": 0, + "step": 1 + } + }, + "cur_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": 0, + "max": 80000, + "scale": 1, + "step": 1 + } + }, + "cur_voltage": { + "type": "Integer", + "value": { + "unit": "V", + "min": 0, + "max": 5000, + "scale": 1, + "step": 1 + } + }, + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + }, + "light_mode": { + "type": "Enum", + "value": { + "range": ["relay", "pos", "none"] + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "cycle_time": { + "type": "String", + "value": {} + }, + "random_time": { + "type": "String", + "value": {} + }, + "switch_inching": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_1": true, + "switch_2": true, + "countdown_1": 0, + "countdown_2": 0, + "add_ele": 1, + "cur_current": 0, + "cur_power": 0, + "cur_voltage": 2471, + "relay_status": "power_on", + "light_mode": "none", + "child_lock": false, + "cycle_time": "", + "random_time": "", + "switch_inching": "" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json index 98843da5614..6e68b1a92db 100644 --- a/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json +++ b/tests/components/tuya/fixtures/pir_3amxzozho9xp4mkh.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "73486068483fda10d633", "name": "rat trap hedge", "category": "pir", "product_id": "3amxzozho9xp4mkh", diff --git a/tests/components/tuya/fixtures/pir_fcdjzz3s.json b/tests/components/tuya/fixtures/pir_fcdjzz3s.json index 65740a4106c..74f223ee7ea 100644 --- a/tests/components/tuya/fixtures/pir_fcdjzz3s.json +++ b/tests/components/tuya/fixtures/pir_fcdjzz3s.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf445324326cbde7c5rg7b", "name": "Motion sensor lidl zigbee", "category": "pir", "product_id": "fcdjzz3s", diff --git a/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json index e4122ee5f9d..8bf85a1d339 100644 --- a/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json +++ b/tests/components/tuya/fixtures/pir_wqz93nrdomectyoz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "20401777500291cfe3a2", "name": "PIR outside stairs", "category": "pir", "product_id": "wqz93nrdomectyoz", diff --git a/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json b/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json index 6cae732aedf..97c4a21526c 100644 --- a/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json +++ b/tests/components/tuya/fixtures/qccdz_7bvgooyjhiua1yyq.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf83514d9c14b426f0fz5y", "name": "AC charging control box", "category": "qccdz", "product_id": "7bvgooyjhiua1yyq", diff --git a/tests/components/tuya/fixtures/qn_5ls2jw49hpczwqng.json b/tests/components/tuya/fixtures/qn_5ls2jw49hpczwqng.json new file mode 100644 index 00000000000..37f16b0d40a --- /dev/null +++ b/tests/components/tuya/fixtures/qn_5ls2jw49hpczwqng.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Mr. Pure", + "category": "qn", + "product_id": "5ls2jw49hpczwqng", + "product_name": "Mr. Pure", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-08-08T12:47:09+00:00", + "create_time": "2025-08-08T12:47:09+00:00", + "update_time": "2025-08-08T12:47:09+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json b/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json index c538630c542..549e23cc914 100644 --- a/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json +++ b/tests/components/tuya/fixtures/qxj_fsea1lat3vuktbt6.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf84c743a84eb2c8abeurz", "name": "BR 7-in-1 WLAN Wetterstation Anthrazit", "category": "qxj", "product_id": "fsea1lat3vuktbt6", diff --git a/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json b/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json index efffe12a2f9..93b3aa580a0 100644 --- a/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json +++ b/tests/components/tuya/fixtures/qxj_is2indt9nlth6esa.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bff00f6abe0563b284t77p", "name": "Frysen", "category": "qxj", "product_id": "is2indt9nlth6esa", diff --git a/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json b/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json index 24b4dbda594..6516626d789 100644 --- a/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json +++ b/tests/components/tuya/fixtures/rqbj_4iqe2hsfyd86kwwc.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaus.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "ebb9d0eb5014f98cfboxbz", "name": "Gas sensor", "category": "rqbj", "product_id": "4iqe2hsfyd86kwwc", diff --git a/tests/components/tuya/fixtures/sd_i6hyjg3af7doaswm.json b/tests/components/tuya/fixtures/sd_i6hyjg3af7doaswm.json new file mode 100644 index 00000000000..15aab08ab4a --- /dev/null +++ b/tests/components/tuya/fixtures/sd_i6hyjg3af7doaswm.json @@ -0,0 +1,64 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Hoover", + "category": "sd", + "product_id": "i6hyjg3af7doaswm", + "product_name": "E20", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-05-05T15:25:07+00:00", + "create_time": "2023-05-05T15:25:07+00:00", + "update_time": "2023-05-05T15:25:07+00:00", + "function": { + "power": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["random", "smart", "wall_follow", "chargego"] + } + }, + "power_go": { + "type": "Boolean", + "value": {} + }, + "seek": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "power": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["random", "smart", "wall_follow", "chargego"] + } + }, + "power_go": { + "type": "Boolean", + "value": {} + }, + "seek": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "power": true, + "mode": "chargego", + "power_go": false, + "seek": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json b/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json index 77d94cb951b..ba461a6226d 100644 --- a/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json +++ b/tests/components/tuya/fixtures/sd_lr33znaodtyarrrz.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfa951ca98fcf64fddqlmt", "name": "V20", "category": "sd", "product_id": "lr33znaodtyarrrz", diff --git a/tests/components/tuya/fixtures/sfkzq_1fcnd8xk.json b/tests/components/tuya/fixtures/sfkzq_1fcnd8xk.json new file mode 100644 index 00000000000..c9ccae70d21 --- /dev/null +++ b/tests/components/tuya/fixtures/sfkzq_1fcnd8xk.json @@ -0,0 +1,130 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Valve Controller 2", + "category": "sfkzq", + "product_id": "1fcnd8xk", + "product_name": "Valve Controller", + "online": false, + "sub": true, + "time_zone": "+01:00", + "active_time": "2023-07-16T09:37:13+00:00", + "create_time": "2023-07-16T09:37:13+00:00", + "update_time": "2023-07-16T09:37:13+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "idle"] + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + }, + "use_time_one": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "battery_percentage": 100, + "time_use": 14025, + "weather_delay": "cancel", + "countdown": 0, + "work_state": "idle", + "smart_weather": "sunny", + "use_time_one": 2 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sfkzq_d4vpmigg.json b/tests/components/tuya/fixtures/sfkzq_d4vpmigg.json new file mode 100644 index 00000000000..922950a358b --- /dev/null +++ b/tests/components/tuya/fixtures/sfkzq_d4vpmigg.json @@ -0,0 +1,130 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Garden Valve Yard", + "category": "sfkzq", + "product_id": "d4vpmigg", + "product_name": "Valve Controller", + "online": true, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-09T19:01:51+00:00", + "create_time": "2025-08-09T19:01:51+00:00", + "update_time": "2025-08-09T19:01:51+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "idle"] + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + }, + "use_time_one": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "battery_percentage": 100, + "time_use": 38201, + "weather_delay": "cancel", + "countdown": 0, + "work_state": "idle", + "smart_weather": "sunny", + "use_time_one": 237 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sfkzq_ed7frwissyqrejic.json b/tests/components/tuya/fixtures/sfkzq_ed7frwissyqrejic.json new file mode 100644 index 00000000000..e301e25930f --- /dev/null +++ b/tests/components/tuya/fixtures/sfkzq_ed7frwissyqrejic.json @@ -0,0 +1,282 @@ +{ + "endpoint": "https://apigw.tuyacn.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "\u63a5HA\u6c34\u9600", + "category": "sfkzq", + "product_id": "ed7frwissyqrejic", + "product_name": "\u63a5HA\u6c34\u9600", + "online": true, + "sub": false, + "time_zone": "+08:00", + "active_time": "2025-07-14T06:32:48+00:00", + "create_time": "2025-07-14T06:32:48+00:00", + "update_time": "2025-07-14T06:32:48+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "switch_3": { + "type": "Boolean", + "value": {} + }, + "switch_4": { + "type": "Boolean", + "value": {} + }, + "switch_5": { + "type": "Boolean", + "value": {} + }, + "switch_6": { + "type": "Boolean", + "value": {} + }, + "switch_7": { + "type": "Boolean", + "value": {} + }, + "switch_8": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_3": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_4": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_5": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_6": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_7": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_8": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "switch_3": { + "type": "Boolean", + "value": {} + }, + "switch_4": { + "type": "Boolean", + "value": {} + }, + "switch_5": { + "type": "Boolean", + "value": {} + }, + "switch_6": { + "type": "Boolean", + "value": {} + }, + "switch_7": { + "type": "Boolean", + "value": {} + }, + "switch_8": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_2": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_3": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_4": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_5": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_6": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_7": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "countdown_8": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 1439, + "scale": 0, + "step": 1 + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "switch_1": true, + "switch_2": false, + "switch_3": true, + "switch_4": false, + "switch_5": true, + "switch_6": false, + "switch_7": true, + "switch_8": false, + "countdown_1": 1, + "countdown_2": 1, + "countdown_3": 3, + "countdown_4": 2, + "countdown_5": 2, + "countdown_6": 3, + "countdown_7": 1, + "countdown_8": 1, + "battery_percentage": 0, + "battery_state": "low" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sfkzq_nxquc5lb.json b/tests/components/tuya/fixtures/sfkzq_nxquc5lb.json new file mode 100644 index 00000000000..8ec1e229b85 --- /dev/null +++ b/tests/components/tuya/fixtures/sfkzq_nxquc5lb.json @@ -0,0 +1,130 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smart Water Timer", + "category": "sfkzq", + "product_id": "nxquc5lb", + "product_name": "Smart Water Timer", + "online": false, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-08T20:15:50+00:00", + "create_time": "2025-08-08T20:15:50+00:00", + "update_time": "2025-08-08T20:15:50+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "idle"] + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + }, + "use_time_one": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "battery_percentage": 100, + "time_use": 2, + "weather_delay": "cancel", + "countdown": 599, + "work_state": "idle", + "smart_weather": "sunny", + "use_time_one": 2 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json b/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json index e57e9274690..30eff8b5c8b 100644 --- a/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json +++ b/tests/components/tuya/fixtures/sfkzq_o6dagifntoafakst.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb9bfc18eeaed2d85yt5m", "name": "Sprinkler Cesare", "category": "sfkzq", "product_id": "o6dagifntoafakst", diff --git a/tests/components/tuya/fixtures/sfkzq_rzklytdei8i8vo37.json b/tests/components/tuya/fixtures/sfkzq_rzklytdei8i8vo37.json new file mode 100644 index 00000000000..2cfcf00cd53 --- /dev/null +++ b/tests/components/tuya/fixtures/sfkzq_rzklytdei8i8vo37.json @@ -0,0 +1,100 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "balkonbew\u00e4sserung", + "category": "sfkzq", + "product_id": "rzklytdei8i8vo37", + "product_name": "Smart Water Timer", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-04-29T17:45:56+00:00", + "create_time": "2025-04-29T17:45:56+00:00", + "update_time": "2025-04-29T17:45:56+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "cycle_time": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "idle"] + } + }, + "use_time_one": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "battery_percentage": 90, + "weather_delay": "cancel", + "countdown": 0, + "work_state": "idle", + "use_time_one": 52 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sgbj_im2eqqhj72suwwko.json b/tests/components/tuya/fixtures/sgbj_im2eqqhj72suwwko.json new file mode 100644 index 00000000000..1ceff88bb55 --- /dev/null +++ b/tests/components/tuya/fixtures/sgbj_im2eqqhj72suwwko.json @@ -0,0 +1,92 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Siren", + "category": "sgbj", + "product_id": "im2eqqhj72suwwko", + "product_name": "Outdoor siren", + "online": true, + "sub": false, + "time_zone": "+10:00", + "active_time": "2025-08-24T05:42:34+00:00", + "create_time": "2025-08-24T05:42:34+00:00", + "update_time": "2025-08-24T05:42:34+00:00", + "function": { + "alarm_state": { + "type": "Enum", + "value": { + "range": ["alarm_sound", "alarm_light", "alarm_sound_light", "normal"] + } + }, + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 10, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "alarm_state": { + "type": "Enum", + "value": { + "range": ["alarm_sound", "alarm_light", "alarm_sound_light", "normal"] + } + }, + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "charge_state": { + "type": "Boolean", + "value": {} + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temper_alarm": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "alarm_state": "normal", + "alarm_volume": "low", + "charge_state": true, + "alarm_time": 1, + "battery_percentage": 77, + "temper_alarm": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json index a3068983c87..b0fd9d38bdf 100644 --- a/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json +++ b/tests/components/tuya/fixtures/sgbj_ulv4nnue7gqp0rjk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0984adfeffe10d5a3ofd", "name": "Siren veranda ", "category": "sgbj", "product_id": "ulv4nnue7gqp0rjk", diff --git a/tests/components/tuya/fixtures/sj_rzeSU2h9uoklxEwq.json b/tests/components/tuya/fixtures/sj_rzeSU2h9uoklxEwq.json new file mode 100644 index 00000000000..1db78307f0d --- /dev/null +++ b/tests/components/tuya/fixtures/sj_rzeSU2h9uoklxEwq.json @@ -0,0 +1,41 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Inondation", + "category": "sj", + "product_id": "rzeSU2h9uoklxEwq", + "product_name": "WATER SENSOR", + "online": false, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-08-13T16:05:44+00:00", + "create_time": "2025-08-13T16:05:44+00:00", + "update_time": "2025-08-13T16:05:44+00:00", + "function": {}, + "status_range": { + "watersensor_state": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "watersensor_state": 2, + "battery_percentage": 100 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sj_tgvtvdoc.json b/tests/components/tuya/fixtures/sj_tgvtvdoc.json new file mode 100644 index 00000000000..bba2d80da88 --- /dev/null +++ b/tests/components/tuya/fixtures/sj_tgvtvdoc.json @@ -0,0 +1,41 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Tournesol", + "category": "sj", + "product_id": "tgvtvdoc", + "product_name": "Rain sensor", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-30T04:51:12+00:00", + "create_time": "2025-07-30T04:51:12+00:00", + "update_time": "2025-07-30T04:51:12+00:00", + "function": {}, + "status_range": { + "watersensor_state": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "watersensor_state": "normal", + "battery_percentage": 98 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json index a6543eac5ea..ed30e930e2b 100644 --- a/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json +++ b/tests/components/tuya/fixtures/sp_drezasavompxpcgm.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf7b8e59f8cd49f425mmfm", "name": "CAM GARAGE", "category": "sp", "product_id": "drezasavompxpcgm", diff --git a/tests/components/tuya/fixtures/sp_nzauwyj3mcnjnf35.json b/tests/components/tuya/fixtures/sp_nzauwyj3mcnjnf35.json new file mode 100644 index 00000000000..21d6b7db1d1 --- /dev/null +++ b/tests/components/tuya/fixtures/sp_nzauwyj3mcnjnf35.json @@ -0,0 +1,220 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Garage Camera", + "category": "sp", + "product_id": "nzauwyj3mcnjnf35", + "product_name": "Smart Camera ", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-01-14T11:37:22+00:00", + "create_time": "2024-01-14T11:37:22+00:00", + "update_time": "2024-01-14T11:37:22+00:00", + "function": { + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "basic_private": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "sd_umount": { + "type": "Boolean", + "value": {} + }, + "motion_record": { + "type": "Boolean", + "value": {} + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + }, + "motion_tracking": { + "type": "Boolean", + "value": {} + }, + "motion_area_switch": { + "type": "Boolean", + "value": {} + }, + "motion_area": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "basic_private": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_storge": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "sd_status": { + "type": "Integer", + "value": { + "min": 1, + "max": 5, + "scale": 0, + "step": 1 + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "sd_umount": { + "type": "Boolean", + "value": {} + }, + "motion_record": { + "type": "Boolean", + "value": {} + }, + "movement_detect_pic": { + "type": "Raw", + "value": {} + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "sd_format_state": { + "type": "Integer", + "value": { + "min": -20000, + "max": 200000, + "scale": 0, + "step": 1 + } + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + }, + "motion_tracking": { + "type": "Boolean", + "value": {} + }, + "motion_area_switch": { + "type": "Boolean", + "value": {} + }, + "motion_area": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "alarm_message": { + "type": "String", + "value": {} + } + }, + "status": { + "basic_flip": true, + "basic_osd": true, + "basic_private": false, + "motion_sensitivity": 0, + "sd_storge": "896|896|0", + "sd_status": 5, + "sd_format": false, + "sd_umount": false, + "motion_record": false, + "movement_detect_pic": "**REDACTED**", + "ptz_stop": true, + "sd_format_state": 0, + "ptz_control": 3, + "motion_switch": false, + "record_switch": true, + "record_mode": 1, + "motion_tracking": false, + "motion_area_switch": true, + "motion_area": { + "num": 1, + "region0": { + "x": 0, + "y": 0, + "xlen": 100, + "ylen": 100 + } + }, + "alarm_message": "**REDACTED**" + }, + "set_up": true, + "support_local": false +} diff --git a/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json index 9a7bb9f1eca..6825c67efc2 100644 --- a/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json +++ b/tests/components/tuya/fixtures/sp_rjKXWRohlvOTyLBu.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf9d5b7ea61ea4c9a6rom9", "name": "CAM PORCH", "category": "sp", "product_id": "rjKXWRohlvOTyLBu", diff --git a/tests/components/tuya/fixtures/sp_rudejjigkywujjvs.json b/tests/components/tuya/fixtures/sp_rudejjigkywujjvs.json new file mode 100644 index 00000000000..06d3f0a2705 --- /dev/null +++ b/tests/components/tuya/fixtures/sp_rudejjigkywujjvs.json @@ -0,0 +1,240 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "B\u00fcrocam", + "category": "sp", + "product_id": "rudejjigkywujjvs", + "product_name": "LSC PTZ Camera", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-15T13:19:09+00:00", + "create_time": "2025-03-15T13:19:09+00:00", + "update_time": "2025-03-15T13:19:09+00:00", + "function": { + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "basic_private": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "basic_nightvision": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "ptz_calibration": { + "type": "Boolean", + "value": {} + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + }, + "siren_switch": { + "type": "Boolean", + "value": {} + }, + "motion_tracking": { + "type": "Boolean", + "value": {} + }, + "basic_anti_flicker": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + } + }, + "status_range": { + "basic_flip": { + "type": "Boolean", + "value": {} + }, + "basic_osd": { + "type": "Boolean", + "value": {} + }, + "basic_private": { + "type": "Boolean", + "value": {} + }, + "motion_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "basic_nightvision": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + }, + "sd_storge": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "sd_status": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 5, + "scale": 1, + "step": 1 + } + }, + "sd_format": { + "type": "Boolean", + "value": {} + }, + "movement_detect_pic": { + "type": "Raw", + "value": {} + }, + "ptz_stop": { + "type": "Boolean", + "value": {} + }, + "sd_format_state": { + "type": "Integer", + "value": { + "unit": "", + "min": -20000, + "max": 20000, + "scale": 1, + "step": 1 + } + }, + "ptz_control": { + "type": "Enum", + "value": { + "range": ["1", "2", "3", "4", "5", "6", "7", "0"] + } + }, + "ptz_calibration": { + "type": "Boolean", + "value": {} + }, + "motion_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_switch": { + "type": "Boolean", + "value": {} + }, + "decibel_sensitivity": { + "type": "Enum", + "value": { + "range": ["0", "1"] + } + }, + "record_switch": { + "type": "Boolean", + "value": {} + }, + "record_mode": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + }, + "siren_switch": { + "type": "Boolean", + "value": {} + }, + "motion_tracking": { + "type": "Boolean", + "value": {} + }, + "alarm_message": { + "type": "String", + "value": {} + }, + "basic_anti_flicker": { + "type": "Enum", + "value": { + "range": ["0", "1", "2"] + } + } + }, + "status": { + "basic_flip": false, + "basic_osd": true, + "basic_private": true, + "motion_sensitivity": 1, + "basic_nightvision": 0, + "sd_storge": "30956544|30674944|281600", + "sd_status": 1, + "sd_format": false, + "movement_detect_pic": "**REDACTED**", + "ptz_stop": false, + "sd_format_state": 0, + "ptz_control": 1, + "ptz_calibration": false, + "motion_switch": true, + "decibel_switch": false, + "decibel_sensitivity": 0, + "record_switch": true, + "record_mode": 1, + "siren_switch": false, + "motion_tracking": false, + "alarm_message": "**REDACTED**", + "basic_anti_flicker": 1 + }, + "set_up": true, + "support_local": false +} diff --git a/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json b/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json index 7e4705650b1..e98e38b21c8 100644 --- a/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json +++ b/tests/components/tuya/fixtures/sp_sdd5f5f2dl5wydjf.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf3f8b448bbc123e29oghf", "name": "C9", "category": "sp", "product_id": "sdd5f5f2dl5wydjf", diff --git a/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json b/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json index fdfbae9fbbf..94a8a7da26f 100644 --- a/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json +++ b/tests/components/tuya/fixtures/tdq_1aegphq4yfd50e6b.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfa008a4f82a56616c69uz", "name": "jardin Fraises", "category": "tdq", "product_id": "1aegphq4yfd50e6b", diff --git a/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json b/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json index e3476118f20..3d7b24df7ec 100644 --- a/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json +++ b/tests/components/tuya/fixtures/tdq_9htyiowaf5rtdhrv.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bff35871a2f4430058vs8u", "name": "Framboisiers", "category": "tdq", "product_id": "9htyiowaf5rtdhrv", diff --git a/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json b/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json index e7c79f3fb41..844f8cd3742 100644 --- a/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json +++ b/tests/components/tuya/fixtures/tdq_cq1p0nt0a4rixnex.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf082711d275c0c883vb4p", "name": "4-433", "category": "tdq", "product_id": "cq1p0nt0a4rixnex", diff --git a/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json b/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json index 1e40823b93d..e1f0865658f 100644 --- a/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json +++ b/tests/components/tuya/fixtures/tdq_nockvv2k39vbrxxk.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyain.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "d7ca553b5f406266350poc", "name": "Seating side 6-ch Smart Switch ", "category": "tdq", "product_id": "nockvv2k39vbrxxk", diff --git a/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json b/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json index da26a133014..cc8d186513c 100644 --- a/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json +++ b/tests/components/tuya/fixtures/tdq_pu8uhxhwcp3tgoz7.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf0dc19ab84dc3627ep2un", "name": "Socket3", "category": "tdq", "product_id": "pu8uhxhwcp3tgoz7", diff --git a/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json b/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json index 708764184ad..54a8d78d92d 100644 --- a/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json +++ b/tests/components/tuya/fixtures/tdq_uoa3mayicscacseb.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb3c90d87dac93d2bdxn3", "name": "Living room left", "category": "tdq", "product_id": "uoa3mayicscacseb", diff --git a/tests/components/tuya/fixtures/tyndj_pyakuuoc.json b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json index 656c626c4fe..ce8ab6c1d63 100644 --- a/tests/components/tuya/fixtures/tyndj_pyakuuoc.json +++ b/tests/components/tuya/fixtures/tyndj_pyakuuoc.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfdb773e4ae317e3915h2i", "name": "Solar zijpad", "category": "tyndj", "product_id": "pyakuuoc", diff --git a/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json index 2fa798b2f60..7fedfb4826e 100644 --- a/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json +++ b/tests/components/tuya/fixtures/wfcon_b25mh8sxawsgndck.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf63312cdd4722555bmsuv", "name": "ZigBee Gateway", "category": "wfcon", "product_id": "b25mh8sxawsgndck", diff --git a/tests/components/tuya/fixtures/wfcon_lieerjyy6l4ykjor.json b/tests/components/tuya/fixtures/wfcon_lieerjyy6l4ykjor.json new file mode 100644 index 00000000000..d30da9ff29b --- /dev/null +++ b/tests/components/tuya/fixtures/wfcon_lieerjyy6l4ykjor.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Zigbee Gateway", + "category": "wfcon", + "product_id": "lieerjyy6l4ykjor", + "product_name": "Zigbee Smart Gateway", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2023-09-10T17:09:13+00:00", + "create_time": "2023-09-10T17:09:13+00:00", + "update_time": "2023-09-10T17:09:13+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wg2_2gowdgni.json b/tests/components/tuya/fixtures/wg2_2gowdgni.json new file mode 100644 index 00000000000..29b5204f72e --- /dev/null +++ b/tests/components/tuya/fixtures/wg2_2gowdgni.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Mesh-Gateway", + "category": "wg2", + "product_id": "2gowdgni", + "product_name": "Mesh-Gateway", + "online": true, + "sub": true, + "time_zone": "-05:00", + "active_time": "2024-07-29T18:45:22+00:00", + "create_time": "2024-07-29T18:45:22+00:00", + "update_time": "2024-07-29T18:45:22+00:00", + "function": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "muffling": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "muffling": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status": { + "switch_alarm_sound": false, + "muffling": false, + "master_state": "normal", + "factory_reset": false, + "alarm_active": "" + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wg2_haclbl0qkqlf2qds.json b/tests/components/tuya/fixtures/wg2_haclbl0qkqlf2qds.json new file mode 100644 index 00000000000..7b5a5e2dece --- /dev/null +++ b/tests/components/tuya/fixtures/wg2_haclbl0qkqlf2qds.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Home Gateway", + "category": "wg2", + "product_id": "haclbl0qkqlf2qds", + "product_name": "Multi-mode Gateway", + "online": false, + "sub": true, + "time_zone": "+03:00", + "active_time": "2025-08-03T10:30:30+00:00", + "create_time": "2025-08-03T10:30:30+00:00", + "update_time": "2025-08-03T10:30:30+00:00", + "function": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "muffling": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "muffling": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status": { + "switch_alarm_sound": false, + "muffling": false, + "master_state": "normal", + "factory_reset": false, + "alarm_active": "" + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json b/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json index 0e39f713dd0..2fb4e9a6064 100644 --- a/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json +++ b/tests/components/tuya/fixtures/wg2_nwxr8qcu4seltoro.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "1752690839034sq255y", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf79ca977d67322eb2o68m", "name": "X5 Zigbee Gateway", "category": "wg2", "product_id": "nwxr8qcu4seltoro", diff --git a/tests/components/tuya/fixtures/wg2_setmxeqgs63xwopm.json b/tests/components/tuya/fixtures/wg2_setmxeqgs63xwopm.json new file mode 100644 index 00000000000..54cc51114c5 --- /dev/null +++ b/tests/components/tuya/fixtures/wg2_setmxeqgs63xwopm.json @@ -0,0 +1,68 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Gateway", + "category": "wg2", + "product_id": "setmxeqgs63xwopm", + "product_name": "", + "online": true, + "sub": true, + "time_zone": "+00:00", + "active_time": "2021-11-26T13:33:58+00:00", + "create_time": "2021-11-26T13:33:58+00:00", + "update_time": "2021-11-26T13:33:58+00:00", + "function": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status": { + "switch_alarm_sound": false, + "master_state": "normal", + "factory_reset": false, + "alarm_active": "" + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wg2_tmwhss6ntjfc7prs.json b/tests/components/tuya/fixtures/wg2_tmwhss6ntjfc7prs.json new file mode 100644 index 00000000000..ee188017887 --- /dev/null +++ b/tests/components/tuya/fixtures/wg2_tmwhss6ntjfc7prs.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Gateway", + "category": "wg2", + "product_id": "tmwhss6ntjfc7prs", + "product_name": "Gateway", + "online": false, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-08T19:16:25+00:00", + "create_time": "2025-08-08T19:16:25+00:00", + "update_time": "2025-08-08T19:16:25+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wg2_v7owd9tzcaninc36.json b/tests/components/tuya/fixtures/wg2_v7owd9tzcaninc36.json new file mode 100644 index 00000000000..dd8bbbbec2b --- /dev/null +++ b/tests/components/tuya/fixtures/wg2_v7owd9tzcaninc36.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Gateway2", + "category": "wg2", + "product_id": "v7owd9tzcaninc36", + "product_name": "Gateway", + "online": false, + "sub": true, + "time_zone": "+01:00", + "active_time": "2023-07-16T09:32:51+00:00", + "create_time": "2023-07-16T09:32:51+00:00", + "update_time": "2023-07-16T09:32:51+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wk_6kijc7nd.json b/tests/components/tuya/fixtures/wk_6kijc7nd.json new file mode 100644 index 00000000000..552de66c1d9 --- /dev/null +++ b/tests/components/tuya/fixtures/wk_6kijc7nd.json @@ -0,0 +1,187 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Кабінет", + "category": "wk", + "product_id": "6kijc7nd", + "product_name": "Thermostat Tervix Pro Line ZigBee color", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-01-22T11:54:29+00:00", + "create_time": "2025-01-22T11:54:29+00:00", + "update_time": "2025-01-22T11:54:29+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["manual", "program"] + } + }, + "window_check": { + "type": "Boolean", + "value": {} + }, + "frost": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 50, + "max": 950, + "scale": 1, + "step": 5 + } + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 350, + "max": 950, + "scale": 1, + "step": 5 + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "℃", + "min": -9, + "max": 9, + "scale": 0, + "step": 1 + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "sensor_choose": { + "type": "Enum", + "value": { + "range": ["in", "out"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["manual", "program"] + } + }, + "window_check": { + "type": "Boolean", + "value": {} + }, + "frost": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 50, + "max": 950, + "scale": 1, + "step": 5 + } + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 350, + "max": 950, + "scale": 1, + "step": 5 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 0, + "max": 995, + "scale": 1, + "step": 5 + } + }, + "window_state": { + "type": "Enum", + "value": { + "range": ["close", "open"] + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "℃", + "min": -9, + "max": 9, + "scale": 0, + "step": 1 + } + }, + "humidity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "sensor_choose": { + "type": "Enum", + "value": { + "range": ["in", "out"] + } + } + }, + "status": { + "switch": true, + "mode": "manual", + "window_check": false, + "frost": false, + "temp_set": 215, + "upper_temp": 450, + "temp_current": 195, + "window_state": "close", + "temp_correction": -2, + "humidity": 23, + "factory_reset": false, + "child_lock": false, + "sensor_choose": "all" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wk_aqoouq7x.json b/tests/components/tuya/fixtures/wk_aqoouq7x.json index 900ae356f38..3bf17e356ff 100644 --- a/tests/components/tuya/fixtures/wk_aqoouq7x.json +++ b/tests/components/tuya/fixtures/wk_aqoouq7x.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf6fc1645146455a2efrex", "name": "Clima cucina", "category": "wk", "product_id": "aqoouq7x", diff --git a/tests/components/tuya/fixtures/wk_ccpwojhalfxryigz.json b/tests/components/tuya/fixtures/wk_ccpwojhalfxryigz.json new file mode 100644 index 00000000000..ed489927c1e --- /dev/null +++ b/tests/components/tuya/fixtures/wk_ccpwojhalfxryigz.json @@ -0,0 +1,121 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Boiler Temperature Controller", + "category": "wk", + "product_id": "ccpwojhalfxryigz", + "product_name": "Intelligent temperature controller", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2024-01-24T19:20:54+00:00", + "create_time": "2024-01-24T19:20:54+00:00", + "update_time": "2024-01-24T19:20:54+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -400, + "max": 1200, + "scale": 1, + "step": 1 + } + }, + "lower_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -400, + "max": 1200, + "scale": 1, + "step": 1 + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "", + "min": -99, + "max": 99, + "scale": 1, + "step": 1 + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["cold", "hot"] + } + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -400, + "max": 1200, + "scale": 1, + "step": 1 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -400, + "max": 1200, + "scale": 1, + "step": 1 + } + }, + "lower_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -400, + "max": 1200, + "scale": 1, + "step": 1 + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "", + "min": -99, + "max": 99, + "scale": 1, + "step": 1 + } + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["e1", "e2", "e3"] + } + } + }, + "status": { + "switch": true, + "work_state": "hot", + "upper_temp": 585, + "temp_current": 575, + "lower_temp": 600, + "temp_correction": -8, + "fault": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json b/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json index 002b0609464..f7c28db1043 100644 --- a/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json +++ b/tests/components/tuya/fixtures/wk_fi6dne5tu4t1nm6j.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfb45cb8a9452fba66lexg", "name": "WiFi Smart Gas Boiler Thermostat ", "category": "wk", "product_id": "fi6dne5tu4t1nm6j", diff --git a/tests/components/tuya/fixtures/wk_gc1bxoq2hafxpa35.json b/tests/components/tuya/fixtures/wk_gc1bxoq2hafxpa35.json new file mode 100644 index 00000000000..23a8607f2d1 --- /dev/null +++ b/tests/components/tuya/fixtures/wk_gc1bxoq2hafxpa35.json @@ -0,0 +1,110 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "\u041f\u043e\u043b\u043e\u0442\u0435\u043d\u0446\u043e\u0441\u0443\u0448\u0438\u0442\u0435\u043b\u044c", + "category": "wk", + "product_id": "gc1bxoq2hafxpa35", + "product_name": "ET-44W", + "online": true, + "sub": false, + "time_zone": "+03:00", + "active_time": "2025-02-08T21:08:00+00:00", + "create_time": "2025-02-08T21:08:00+00:00", + "update_time": "2025-02-08T21:08:00+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["holiday"] + } + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 50, + "max": 400, + "scale": 1, + "step": 5 + } + }, + "temp_set_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 410, + "max": 1040, + "scale": 1, + "step": 10 + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["holiday"] + } + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 50, + "max": 400, + "scale": 1, + "step": 5 + } + }, + "temp_set_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 410, + "max": 1040, + "scale": 1, + "step": 10 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 0, + "max": 450, + "scale": 1, + "step": 5 + } + }, + "temp_current_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 320, + "max": 1130, + "scale": 1, + "step": 10 + } + } + }, + "status": { + "switch": false, + "mode": "hold", + "temp_set": 50, + "temp_set_f": 410, + "temp_current": 253, + "temp_current_f": 320 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wk_gogb05wrtredz3bs.json b/tests/components/tuya/fixtures/wk_gogb05wrtredz3bs.json new file mode 100644 index 00000000000..0841f77ca2c --- /dev/null +++ b/tests/components/tuya/fixtures/wk_gogb05wrtredz3bs.json @@ -0,0 +1,195 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "smart thermostats", + "category": "wk", + "product_id": "gogb05wrtredz3bs", + "product_name": "smart thermostats", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-01-23T13:07:21+00:00", + "create_time": "2025-01-23T13:07:21+00:00", + "update_time": "2025-01-23T13:07:21+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["auto", "manual"] + } + }, + "frost": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 5, + "max": 90, + "scale": 0, + "step": 1 + } + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 30, + "max": 90, + "scale": 0, + "step": 1 + } + }, + "lower_temp": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 5, + "max": 20, + "scale": 0, + "step": 1 + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "摄氏度", + "min": -9, + "max": 9, + "scale": 0, + "step": 1 + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "sensor_choose": { + "type": "Enum", + "value": { + "range": ["in", "out"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["auto", "manual"] + } + }, + "frost": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 5, + "max": 90, + "scale": 0, + "step": 1 + } + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 30, + "max": 90, + "scale": 0, + "step": 1 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 0, + "max": 900, + "scale": 1, + "step": 5 + } + }, + "lower_temp": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 5, + "max": 20, + "scale": 0, + "step": 1 + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "摄氏度", + "min": -9, + "max": 9, + "scale": 0, + "step": 1 + } + }, + "valve_state": { + "type": "Enum", + "value": { + "range": ["open", "close"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "child_lock": { + "type": "Boolean", + "value": {} + }, + "sensor_choose": { + "type": "Enum", + "value": { + "range": ["in", "out"] + } + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["e1", "e2", "e3"] + } + } + }, + "status": { + "switch": false, + "mode": "manual", + "frost": true, + "temp_set": 12, + "upper_temp": 30, + "temp_current": 215, + "lower_temp": 5, + "temp_correction": -2, + "valve_state": "close", + "factory_reset": false, + "child_lock": false, + "sensor_choose": "in", + "fault": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wk_y5obtqhuztqsf2mj.json b/tests/components/tuya/fixtures/wk_y5obtqhuztqsf2mj.json new file mode 100644 index 00000000000..efe02c633f3 --- /dev/null +++ b/tests/components/tuya/fixtures/wk_y5obtqhuztqsf2mj.json @@ -0,0 +1,74 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Term - Prizemi", + "category": "wk", + "product_id": "y5obtqhuztqsf2mj", + "product_name": "Smart", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-02-22T10:25:25+00:00", + "create_time": "2025-02-22T10:25:25+00:00", + "update_time": "2025-02-22T10:25:25+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 5, + "max": 700, + "scale": 1, + "step": 1 + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 5, + "max": 700, + "scale": 1, + "step": 1 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "℃", + "min": 0, + "max": 900, + "scale": 1, + "step": 1 + } + }, + "child_lock": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch": true, + "temp_set": 230, + "temp_current": 230, + "child_lock": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wkcz_gc4b1mdw7kebtuyz.json b/tests/components/tuya/fixtures/wkcz_gc4b1mdw7kebtuyz.json new file mode 100644 index 00000000000..78afaebc51f --- /dev/null +++ b/tests/components/tuya/fixtures/wkcz_gc4b1mdw7kebtuyz.json @@ -0,0 +1,227 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "pid_relay_2", + "category": "wkcz", + "product_id": "gc4b1mdw7kebtuyz", + "product_name": "4-TH", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-01-14T16:04:37+00:00", + "create_time": "2025-01-14T16:04:37+00:00", + "update_time": "2025-01-14T16:04:37+00:00", + "function": { + "switch_all": { + "type": "Boolean", + "value": {} + }, + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -500, + "max": 1100, + "scale": 1, + "step": 1 + } + }, + "lower_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -500, + "max": 1100, + "scale": 1, + "step": 1 + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "", + "min": -90, + "max": 90, + "scale": 1, + "step": 1 + } + }, + "maxhum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 1000, + "scale": 1, + "step": 1 + } + }, + "minihum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 1000, + "scale": 1, + "step": 1 + } + }, + "hum_calibration": { + "type": "Integer", + "value": { + "unit": "%", + "min": -10, + "max": 10, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_all": { + "type": "Boolean", + "value": {} + }, + "switch_1": { + "type": "Boolean", + "value": {} + }, + "switch_2": { + "type": "Boolean", + "value": {} + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -500, + "max": 1100, + "scale": 1, + "step": 1 + } + }, + "upper_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -500, + "max": 1100, + "scale": 1, + "step": 1 + } + }, + "lower_temp": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -500, + "max": 1100, + "scale": 1, + "step": 1 + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 43200, + "scale": 0, + "step": 1 + } + }, + "temp_correction": { + "type": "Integer", + "value": { + "unit": "", + "min": -90, + "max": 90, + "scale": 1, + "step": 1 + } + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["cooling_fault", "heating_fault", "temp_dif_fault"] + } + }, + "humidity_value": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "maxhum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 1000, + "scale": 1, + "step": 1 + } + }, + "minihum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 1000, + "scale": 1, + "step": 1 + } + }, + "hum_calibration": { + "type": "Integer", + "value": { + "unit": "%", + "min": -10, + "max": 10, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_all": false, + "switch_1": false, + "switch_2": false, + "temp_current": 170, + "upper_temp": 0, + "lower_temp": 0, + "countdown_1": 0, + "temp_correction": 0, + "fault": 0, + "humidity_value": 38, + "maxhum_set": 0, + "minihum_set": 0, + "hum_calibration": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wnykq_npbbca46yiug8ysk.json b/tests/components/tuya/fixtures/wnykq_npbbca46yiug8ysk.json new file mode 100644 index 00000000000..2ea3099bc2b --- /dev/null +++ b/tests/components/tuya/fixtures/wnykq_npbbca46yiug8ysk.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Bedroom IR", + "category": "wnykq", + "product_id": "npbbca46yiug8ysk", + "product_name": "Smart IR", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-08-10T17:32:31+00:00", + "create_time": "2023-08-10T17:32:31+00:00", + "update_time": "2023-08-10T17:32:31+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wnykq_om518smspsaltzdi.json b/tests/components/tuya/fixtures/wnykq_om518smspsaltzdi.json new file mode 100644 index 00000000000..537e9604953 --- /dev/null +++ b/tests/components/tuya/fixtures/wnykq_om518smspsaltzdi.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smart IR Theater", + "category": "wnykq", + "product_id": "om518smspsaltzdi", + "product_name": "Smart IR", + "online": false, + "sub": false, + "time_zone": "-05:00", + "active_time": "2025-08-08T21:06:47+00:00", + "create_time": "2025-08-08T21:06:47+00:00", + "update_time": "2025-08-08T21:06:47+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wnykq_rqhxdyusjrwxyff6.json b/tests/components/tuya/fixtures/wnykq_rqhxdyusjrwxyff6.json new file mode 100644 index 00000000000..f2ceac8e898 --- /dev/null +++ b/tests/components/tuya/fixtures/wnykq_rqhxdyusjrwxyff6.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smart IR", + "category": "wnykq", + "product_id": "rqhxdyusjrwxyff6", + "product_name": "Smart IR", + "online": true, + "sub": false, + "time_zone": "+07:00", + "active_time": "2024-07-18T12:07:37+00:00", + "create_time": "2024-07-18T12:07:37+00:00", + "update_time": "2024-07-18T12:07:37+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json b/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json index 2929872f4c1..51367039d9f 100644 --- a/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json +++ b/tests/components/tuya/fixtures/wsdcg_g2y6z3p3ja2qhyav.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf316b8707b061f044th18", "name": "NP DownStairs North", "category": "wsdcg", "product_id": "g2y6z3p3ja2qhyav", diff --git a/tests/components/tuya/fixtures/wsdcg_iq4ygaai.json b/tests/components/tuya/fixtures/wsdcg_iq4ygaai.json new file mode 100644 index 00000000000..d76dae842fa --- /dev/null +++ b/tests/components/tuya/fixtures/wsdcg_iq4ygaai.json @@ -0,0 +1,45 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Bassin", + "category": "wsdcg", + "product_id": "iq4ygaai", + "product_name": "Temperature Sensor", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T12:20:32+00:00", + "create_time": "2025-07-19T12:20:32+00:00", + "update_time": "2025-07-19T12:20:32+00:00", + "function": {}, + "status_range": { + "va_temperature": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 1990, + "scale": 1, + "step": 1 + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "va_temperature": 217, + "battery_percentage": 100 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wsdcg_iv7hudlj.json b/tests/components/tuya/fixtures/wsdcg_iv7hudlj.json new file mode 100644 index 00000000000..b96cb26a1a9 --- /dev/null +++ b/tests/components/tuya/fixtures/wsdcg_iv7hudlj.json @@ -0,0 +1,168 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Basement temperature", + "category": "wsdcg", + "product_id": "iv7hudlj", + "product_name": "Bluetooth Temperature Humidity Sensor", + "online": false, + "sub": false, + "time_zone": "+02:00", + "active_time": "2024-10-13T10:07:37+00:00", + "create_time": "2024-10-13T10:07:37+00:00", + "update_time": "2024-10-13T10:07:37+00:00", + "function": { + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + }, + "maxtemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -99, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "minitemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -99, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "maxhum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "minihum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "va_temperature": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -99, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "va_humidity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + }, + "maxtemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -99, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "minitemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -99, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "maxhum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "minihum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_alarm": { + "type": "Enum", + "value": { + "range": ["loweralarm", "upperalarm", "cancel"] + } + }, + "hum_alarm": { + "type": "Enum", + "value": { + "range": ["loweralarm", "upperalarm", "cancel"] + } + } + }, + "status": { + "va_temperature": 162, + "va_humidity": 47, + "battery_percentage": 100, + "temp_unit_convert": "c", + "maxtemp_set": 400, + "minitemp_set": 200, + "maxhum_set": 85, + "minihum_set": 20, + "temp_alarm": "loweralarm", + "hum_alarm": "cancel" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wsdcg_krlcihrpzpc8olw9.json b/tests/components/tuya/fixtures/wsdcg_krlcihrpzpc8olw9.json new file mode 100644 index 00000000000..023bcc269fa --- /dev/null +++ b/tests/components/tuya/fixtures/wsdcg_krlcihrpzpc8olw9.json @@ -0,0 +1,66 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "IFS-STD002", + "category": "wsdcg", + "product_id": "krlcihrpzpc8olw9", + "product_name": "IFS-STD002", + "online": true, + "sub": false, + "time_zone": "+03:00", + "active_time": "2025-06-28T13:33:51+00:00", + "create_time": "2025-06-28T13:33:51+00:00", + "update_time": "2025-06-28T13:33:51+00:00", + "function": { + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + } + }, + "status_range": { + "va_temperature": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -99, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "va_humidity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + } + }, + "status": { + "va_temperature": 289, + "va_humidity": 61, + "battery_state": "high", + "temp_unit_convert": "c" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wsdcg_lf36y5nwb8jkxwgg.json b/tests/components/tuya/fixtures/wsdcg_lf36y5nwb8jkxwgg.json new file mode 100644 index 00000000000..5c52cf2796e --- /dev/null +++ b/tests/components/tuya/fixtures/wsdcg_lf36y5nwb8jkxwgg.json @@ -0,0 +1,66 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Greenhouse", + "category": "wsdcg", + "product_id": "lf36y5nwb8jkxwgg", + "product_name": "T & H Sensor", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-08-14T13:59:25+00:00", + "create_time": "2023-08-14T13:59:25+00:00", + "update_time": "2023-08-14T13:59:25+00:00", + "function": { + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + } + }, + "status_range": { + "va_temperature": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "va_humidity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 99, + "scale": 0, + "step": 1 + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + } + }, + "status": { + "va_temperature": 322, + "va_humidity": 53, + "battery_state": "middle", + "temp_unit_convert": "c" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wsdcg_vtA4pDd6PLUZzXgZ.json b/tests/components/tuya/fixtures/wsdcg_vtA4pDd6PLUZzXgZ.json new file mode 100644 index 00000000000..1eb84adfc31 --- /dev/null +++ b/tests/components/tuya/fixtures/wsdcg_vtA4pDd6PLUZzXgZ.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Humy bain", + "category": "wsdcg", + "product_id": "vtA4pDd6PLUZzXgZ", + "product_name": "Temperature and humidity sensor", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T12:04:21+00:00", + "create_time": "2025-07-19T12:04:21+00:00", + "update_time": "2025-07-19T12:04:21+00:00", + "function": {}, + "status_range": { + "va_humidity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "va_temperature": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -20, + "max": 60, + "scale": 0, + "step": 1 + } + }, + "va_battery": { + "type": "Integer", + "value": { + "unit": "", + "min": 0, + "max": 500, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "va_humidity": 63, + "va_battery": 100, + "va_temperature": 20 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wsdcg_xr3htd96.json b/tests/components/tuya/fixtures/wsdcg_xr3htd96.json new file mode 100644 index 00000000000..13bdff60b33 --- /dev/null +++ b/tests/components/tuya/fixtures/wsdcg_xr3htd96.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Humy toilettes RDC", + "category": "wsdcg", + "product_id": "xr3htd96", + "product_name": "Temperature Humidity Sensor", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-08-01T09:33:45+00:00", + "create_time": "2025-08-01T09:33:45+00:00", + "update_time": "2025-08-01T09:33:45+00:00", + "function": {}, + "status_range": { + "va_temperature": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "va_humidity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 1000, + "scale": 1, + "step": 1 + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "va_temperature": 206, + "va_humidity": 618, + "battery_percentage": 100 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wsdcg_yqiqbaldtr0i7mru.json b/tests/components/tuya/fixtures/wsdcg_yqiqbaldtr0i7mru.json new file mode 100644 index 00000000000..1186bfb4572 --- /dev/null +++ b/tests/components/tuya/fixtures/wsdcg_yqiqbaldtr0i7mru.json @@ -0,0 +1,259 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": false, + "disabled_by": null, + "disabled_polling": false, + "name": "WiFi Temperature & Humidity Sensor", + "category": "wsdcg", + "product_id": "yqiqbaldtr0i7mru", + "product_name": "WiFi Temperature & Humidity Sensor", + "online": true, + "sub": false, + "time_zone": "+07:00", + "active_time": "2023-11-27T03:59:48+00:00", + "create_time": "2023-11-27T03:59:48+00:00", + "update_time": "2023-11-27T03:59:48+00:00", + "function": { + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + }, + "maxtemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "minitemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "maxhum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "minihum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_periodic_report": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 120, + "scale": 0, + "step": 1 + } + }, + "hum_periodic_report": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 120, + "scale": 0, + "step": 1 + } + }, + "temp_sensitivity": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 3, + "max": 20, + "scale": 1, + "step": 1 + } + }, + "hum_sensitivity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 3, + "max": 20, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "va_temperature": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "va_humidity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + }, + "maxtemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "minitemp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -200, + "max": 600, + "scale": 1, + "step": 1 + } + }, + "maxhum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "minihum_set": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temp_alarm": { + "type": "Enum", + "value": { + "range": ["loweralarm", "upperalarm", "cancel"] + } + }, + "hum_alarm": { + "type": "Enum", + "value": { + "range": ["loweralarm", "upperalarm", "cancel"] + } + }, + "temp_periodic_report": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 120, + "scale": 0, + "step": 1 + } + }, + "hum_periodic_report": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 120, + "scale": 0, + "step": 1 + } + }, + "temp_sensitivity": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 3, + "max": 20, + "scale": 1, + "step": 1 + } + }, + "hum_sensitivity": { + "type": "Integer", + "value": { + "unit": "%", + "min": 3, + "max": 20, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "va_temperature": 251, + "va_humidity": 12, + "battery_state": "high", + "battery_percentage": 100, + "temp_unit_convert": "c", + "maxtemp_set": 390, + "minitemp_set": 0, + "maxhum_set": 60, + "minihum_set": 20, + "temp_alarm": "cancel", + "hum_alarm": "loweralarm", + "temp_periodic_report": 60, + "hum_periodic_report": 120, + "temp_sensitivity": 6, + "hum_sensitivity": 6 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wxkg_ja5osu5g.json b/tests/components/tuya/fixtures/wxkg_ja5osu5g.json new file mode 100644 index 00000000000..d8e841fc599 --- /dev/null +++ b/tests/components/tuya/fixtures/wxkg_ja5osu5g.json @@ -0,0 +1,64 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Bouton tempo ext\u00e9rieur", + "category": "wxkg", + "product_id": "ja5osu5g", + "product_name": "ZC-YED-\u4e00\u952e\u65e0\u7ebf\u5f00\u5173", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-25T15:49:27+00:00", + "create_time": "2025-07-25T15:49:27+00:00", + "update_time": "2025-07-25T15:49:27+00:00", + "function": { + "mode": { + "type": "Enum", + "value": { + "range": ["remote_control", "wireless_switch"] + } + }, + "scene_preset": { + "type": "String", + "value": {} + } + }, + "status_range": { + "switch_mode1": { + "type": "Enum", + "value": { + "range": ["click", "double_click", "press"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["remote_control", "wireless_switch"] + } + }, + "scene_preset": { + "type": "String", + "value": {} + } + }, + "status": { + "switch_mode1": "press", + "battery_percentage": 100, + "mode": "wireless_switch", + "scene_preset": "800003e80384005a03e803e8810003e8006400b403e803e8820001900320010e03e803e883010190000000f000fa00fa" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json b/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json index a7ab15a4511..5fd511c7506 100644 --- a/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json +++ b/tests/components/tuya/fixtures/ydkt_jevroj5aguwdbs2e.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "mock_device_id", "name": "DOLCECLIMA 10 HP WIFI", "category": "ydkt", "product_id": "jevroj5aguwdbs2e", diff --git a/tests/components/tuya/fixtures/ygsb_l6ax0u6jwbz82atk.json b/tests/components/tuya/fixtures/ygsb_l6ax0u6jwbz82atk.json new file mode 100644 index 00000000000..1f517f9b775 --- /dev/null +++ b/tests/components/tuya/fixtures/ygsb_l6ax0u6jwbz82atk.json @@ -0,0 +1,63 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Pond", + "category": "ygsb", + "product_id": "l6ax0u6jwbz82atk", + "product_name": "\u6c34\u6cf5", + "online": true, + "sub": false, + "time_zone": "-04:00", + "active_time": "2025-06-07T22:22:44+00:00", + "create_time": "2025-06-07T22:22:44+00:00", + "update_time": "2025-06-07T22:22:44+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "water_flow": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 10 + } + }, + "pause": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "water_flow": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 10 + } + }, + "pause": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch": false, + "water_flow": 0, + "pause": true + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ykq_bngwdjsr.json b/tests/components/tuya/fixtures/ykq_bngwdjsr.json new file mode 100644 index 00000000000..085dd52b6cb --- /dev/null +++ b/tests/components/tuya/fixtures/ykq_bngwdjsr.json @@ -0,0 +1,70 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "T\u00e9l\u00e9commande lumi\u00e8res ZigBee", + "category": "ykq", + "product_id": "bngwdjsr", + "product_name": "Remote controller", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-19T11:36:45+00:00", + "create_time": "2025-07-19T11:36:45+00:00", + "update_time": "2025-07-19T11:36:45+00:00", + "function": { + "switch_controller": { + "type": "Boolean", + "value": {} + }, + "mode_controller": { + "type": "Enum", + "value": { + "range": ["white", "colour"] + } + }, + "bright_controller": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_controller": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "scene_controller": { + "type": "Enum", + "value": { + "range": ["scene_1", "scene_2", "scene_3", "scene_4"] + } + } + }, + "status": { + "battery_percentage": 100, + "scene_controller": "scene_1" + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ywbj_arywmw6h6vesoz5t.json b/tests/components/tuya/fixtures/ywbj_arywmw6h6vesoz5t.json new file mode 100644 index 00000000000..eee71d0c45a --- /dev/null +++ b/tests/components/tuya/fixtures/ywbj_arywmw6h6vesoz5t.json @@ -0,0 +1,37 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Rauchmelder Drucker", + "category": "ywbj", + "product_id": "arywmw6h6vesoz5t", + "product_name": "Smoke Alarm ", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2025-03-15T13:45:57+00:00", + "create_time": "2025-03-15T13:45:57+00:00", + "update_time": "2025-03-15T13:45:57+00:00", + "function": {}, + "status_range": { + "smoke_sensor_status": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "smoke_sensor_status": "normal", + "battery_state": "high" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ywbj_cjlutkuuvxnie17o.json b/tests/components/tuya/fixtures/ywbj_cjlutkuuvxnie17o.json new file mode 100644 index 00000000000..6495e99e4d3 --- /dev/null +++ b/tests/components/tuya/fixtures/ywbj_cjlutkuuvxnie17o.json @@ -0,0 +1,37 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Rauchmelder Alexsandro ", + "category": "ywbj", + "product_id": "cjlutkuuvxnie17o", + "product_name": "Smoke Alarm", + "online": true, + "sub": false, + "time_zone": "+01:00", + "active_time": "2023-01-07T18:50:02+00:00", + "create_time": "2023-01-07T18:50:02+00:00", + "update_time": "2023-01-07T18:50:02+00:00", + "function": {}, + "status_range": { + "smoke_sensor_status": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + } + }, + "status": { + "smoke_sensor_status": "normal", + "battery_state": "high" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json index f80b0cd5cd1..c39835694c7 100644 --- a/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json +++ b/tests/components/tuya/fixtures/ywbj_gf9dejhmzffgdyfj.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "8670375210521cf1349c", "name": " Smoke detector upstairs ", "category": "ywbj", "product_id": "gf9dejhmzffgdyfj", diff --git a/tests/components/tuya/fixtures/ywbj_kscbebaf3s1eogvt.json b/tests/components/tuya/fixtures/ywbj_kscbebaf3s1eogvt.json new file mode 100644 index 00000000000..00e5db9dc94 --- /dev/null +++ b/tests/components/tuya/fixtures/ywbj_kscbebaf3s1eogvt.json @@ -0,0 +1,51 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "WIFI Smoke alarm", + "category": "ywbj", + "product_id": "kscbebaf3s1eogvt", + "product_name": "WIFI Smoke alarm", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2023-08-16T13:20:39+00:00", + "create_time": "2023-08-16T13:20:39+00:00", + "update_time": "2023-08-16T13:20:39+00:00", + "function": { + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "smoke_sensor_status": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "smoke_sensor_status": "normal", + "battery_percentage": 90, + "muffling": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ywbj_rccxox8p.json b/tests/components/tuya/fixtures/ywbj_rccxox8p.json new file mode 100644 index 00000000000..45a5e8697f2 --- /dev/null +++ b/tests/components/tuya/fixtures/ywbj_rccxox8p.json @@ -0,0 +1,68 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smoke Alarm", + "category": "ywbj", + "product_id": "rccxox8p", + "product_name": "Smoke Alarm", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2024-08-05T13:47:04+00:00", + "create_time": "2024-08-05T13:47:04+00:00", + "update_time": "2024-08-05T13:47:04+00:00", + "function": { + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "smoke_sensor_status": { + "type": "Enum", + "value": { + "range": ["alarm", "normal"] + } + }, + "smoke_sensor_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 1, + "step": 1 + } + }, + "battery_state": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "muffling": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "smoke_sensor_status": "normal", + "smoke_sensor_value": 0, + "battery_state": "low", + "battery_percentage": 100, + "muffling": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json b/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json index 8b1cff0c773..31d26fbb715 100644 --- a/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json +++ b/tests/components/tuya/fixtures/ywcgq_h8lvyoahr6s6aybf.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf3d16d38b17d7034ddxd4", "name": "Rainwater Tank Level", "category": "ywcgq", "product_id": "h8lvyoahr6s6aybf", diff --git a/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json index 52eda664345..200790afedb 100644 --- a/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json +++ b/tests/components/tuya/fixtures/ywcgq_wtzwyhkev3b4ubns.json @@ -135,6 +135,5 @@ "installation_height": 560, "liquid_depth_max": 100, "liquid_level_percent": 100 - }, - "terminal_id": "REDACTED" + } } diff --git a/tests/components/tuya/fixtures/zjq_nkkl7uzv.json b/tests/components/tuya/fixtures/zjq_nkkl7uzv.json new file mode 100644 index 00000000000..043db64dc77 --- /dev/null +++ b/tests/components/tuya/fixtures/zjq_nkkl7uzv.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Zigby r\u00e9p\u00e9teur ", + "category": "zjq", + "product_id": "nkkl7uzv", + "product_name": "Zigbee Repeater", + "online": true, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-07-25T17:19:41+00:00", + "create_time": "2025-07-25T17:19:41+00:00", + "update_time": "2025-07-25T17:19:41+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/zndb_4ggkyflayu1h1ho9.json b/tests/components/tuya/fixtures/zndb_4ggkyflayu1h1ho9.json new file mode 100644 index 00000000000..92f507abaca --- /dev/null +++ b/tests/components/tuya/fixtures/zndb_4ggkyflayu1h1ho9.json @@ -0,0 +1,216 @@ +{ + "endpoint": "https://apigw.tuyacn.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "XOCA-DAC212XC V2-S1", + "category": "zndb", + "product_id": "4ggkyflayu1h1ho9", + "product_name": "XOCA-DAC212XC V2-S1", + "online": true, + "sub": false, + "time_zone": "+08:00", + "active_time": "2025-07-07T10:32:35+00:00", + "create_time": "2025-07-07T10:32:35+00:00", + "update_time": "2025-07-07T10:32:35+00:00", + "function": { + "frozen_time_set": { + "type": "Json", + "value": {} + }, + "switch_prepayment": { + "type": "Boolean", + "value": {} + }, + "clear_energy": { + "type": "Boolean", + "value": {} + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "alarm_set_2": { + "type": "Json", + "value": {} + }, + "event_clear": { + "type": "Boolean", + "value": {} + }, + "price_set": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "forward_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "reverse_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "phase_a": { + "type": "Json", + "value": {} + }, + "fault": { + "type": "Bitmap", + "value": { + "label": [ + "ov_cr", + "unbalance_alarm", + "ov_vol", + "undervoltage_alarm", + "miss_phase_alarm", + "outage_alarm", + "magnetism_alarm", + "terminal_alarm", + "cover_alarm", + "credit_alarm", + "no_balance_alarm", + "battery_alarm", + "meter_hardware_alarm", + "overdraft_unlim", + "arrear_outage", + "overdraft_use", + "pf_abnormal", + "ov_pwr" + ] + } + }, + "frozen_time_set": { + "type": "Json", + "value": {} + }, + "switch_prepayment": { + "type": "Boolean", + "value": {} + }, + "clear_energy": { + "type": "Boolean", + "value": {} + }, + "switch": { + "type": "Boolean", + "value": {} + }, + "alarm_set_2": { + "type": "Json", + "value": {} + }, + "meter_id": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "event_clear": { + "type": "Boolean", + "value": {} + }, + "forward_energy_t1": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "forward_energy_t2": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "forward_energy_t3": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "forward_energy_t4": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "price_set": { + "type": "Raw", + "value": {} + }, + "online_state": { + "type": "Enum", + "value": { + "range": ["offline", "online"] + } + }, + "supply_frequency": { + "type": "Integer", + "value": { + "unit": "Hz", + "min": 0, + "max": 9999, + "scale": 2, + "step": 1 + } + } + }, + "status": { + "forward_energy_total": 120, + "reverse_energy_total": 80, + "phase_a": { + "electricCurrent": 599.552, + "power": 6.912, + "voltage": 52.7 + }, + "fault": 0, + "frozen_time_set": { + "day": 158, + "hour": 233 + }, + "switch_prepayment": false, + "clear_energy": false, + "switch": true, + "alarm_set_2": [], + "meter_id": "", + "event_clear": false, + "forward_energy_t1": 0, + "forward_energy_t2": 0, + "forward_energy_t3": 0, + "forward_energy_t4": 0, + "price_set": "", + "online_state": "offline", + "supply_frequency": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/zndb_v5jlnn5hwyffkhp3.json b/tests/components/tuya/fixtures/zndb_v5jlnn5hwyffkhp3.json new file mode 100644 index 00000000000..01401e16dd3 --- /dev/null +++ b/tests/components/tuya/fixtures/zndb_v5jlnn5hwyffkhp3.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Production", + "category": "zndb", + "product_id": "v5jlnn5hwyffkhp3", + "product_name": "Smart Meter", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-04-18T08:57:59+00:00", + "create_time": "2025-04-18T08:57:59+00:00", + "update_time": "2025-04-18T08:57:59+00:00", + "function": { + "forward_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "reverse_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + } + }, + "status_range": { + "forward_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "reverse_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "total_power": { + "type": "Integer", + "value": { + "unit": "W", + "min": -99999999, + "max": 99999999, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "forward_energy_total": 152021, + "reverse_energy_total": 0, + "total_power": 23146 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json b/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json index 797ddba3587..caf9074d277 100644 --- a/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json +++ b/tests/components/tuya/fixtures/zndb_ze8faryrxr0glqnn.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bfe33b4c74661f1f1bgacy", "name": "Meter", "category": "zndb", "product_id": "ze8faryrxr0glqnn", diff --git a/tests/components/tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json b/tests/components/tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json new file mode 100644 index 00000000000..597721599e3 --- /dev/null +++ b/tests/components/tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Wi-Fi solar grid micro inverter \uff08GT\uff09", + "category": "znnbq", + "product_id": "6b3pbbuqbfabhfiq", + "product_name": "Wi-Fi solar grid micro inverter \uff08GT\uff09", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-08-18T16:19:32+00:00", + "create_time": "2025-08-18T16:19:32+00:00", + "update_time": "2025-08-18T16:19:32+00:00", + "function": {}, + "status_range": { + "reverse_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "power_total": { + "type": "Integer", + "value": { + "unit": "kW", + "min": 0, + "max": 900000, + "scale": 3, + "step": 1 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -500, + "max": 2000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "reverse_energy_total": 19, + "power_total": 0, + "temp_current": 219 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/znrb_db81ge24jctwx8lo.json b/tests/components/tuya/fixtures/znrb_db81ge24jctwx8lo.json new file mode 100644 index 00000000000..6e379eff375 --- /dev/null +++ b/tests/components/tuya/fixtures/znrb_db81ge24jctwx8lo.json @@ -0,0 +1,172 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Hot Water Heat Pump", + "category": "znrb", + "product_id": "db81ge24jctwx8lo", + "product_name": "Heat Pump", + "online": true, + "sub": false, + "time_zone": "+11:00", + "active_time": "2025-01-08T23:48:22+00:00", + "create_time": "2025-01-08T23:48:22+00:00", + "update_time": "2025-01-08T23:48:22+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 15, + "max": 75, + "scale": 0, + "step": 1 + } + }, + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + }, + "defrost": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 15, + "max": 75, + "scale": 0, + "step": 1 + } + }, + "temp_unit_convert": { + "type": "Enum", + "value": { + "range": ["c", "f"] + } + }, + "defrost": { + "type": "Boolean", + "value": {} + }, + "countdown_left": { + "type": "Integer", + "value": { + "unit": "P", + "min": -500, + "max": 500, + "scale": 0, + "step": 1 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -30, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "power_consumption": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 100000, + "scale": 2, + "step": 1 + } + }, + "compressor_strength": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -50, + "max": 140, + "scale": 0, + "step": 1 + } + }, + "temp_top": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -58, + "max": 312, + "scale": 0, + "step": 1 + } + }, + "temp_bottom": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -50, + "max": 150, + "scale": 0, + "step": 1 + } + }, + "compressor_state": { + "type": "Boolean", + "value": {} + }, + "four_valve_state": { + "type": "Boolean", + "value": {} + }, + "draught_fan_state": { + "type": "Boolean", + "value": {} + }, + "pump_state": { + "type": "Boolean", + "value": {} + }, + "ele_heating_state": { + "type": "Boolean", + "value": {} + }, + "defrost_state": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch": false, + "temp_set": 60, + "temp_unit_convert": "c", + "defrost": false, + "countdown_left": 300, + "temp_current": 42, + "power_consumption": 0, + "compressor_strength": 23, + "temp_top": 21, + "temp_bottom": -50, + "compressor_state": false, + "four_valve_state": false, + "draught_fan_state": false, + "pump_state": true, + "ele_heating_state": false, + "defrost_state": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/zwjcy_myd45weu.json b/tests/components/tuya/fixtures/zwjcy_myd45weu.json index 3ea111abb0e..dc6c0510ffc 100644 --- a/tests/components/tuya/fixtures/zwjcy_myd45weu.json +++ b/tests/components/tuya/fixtures/zwjcy_myd45weu.json @@ -1,10 +1,8 @@ { "endpoint": "https://apigw.tuyaeu.com", - "terminal_id": "REDACTED", "mqtt_connected": true, "disabled_by": null, "disabled_polling": false, - "id": "bf1a0431555359ce06ie0z", "name": "Patates", "category": "zwjcy", "product_id": "myd45weu", diff --git a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr index 73072dcb516..337b579c7da 100644 --- a/tests/components/tuya/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/tuya/snapshots/test_alarm_control_panel.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-entry] +# name: test_platform_setup_and_discovery[alarm_control_panel.multifunction_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -30,11 +30,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.123123aba12312312dazubmaster_mode', + 'unique_id': 'tuya.2pxfek1jjrtctiyglammaster_mode', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][alarm_control_panel.multifunction_alarm-state] +# name: test_platform_setup_and_discovery[alarm_control_panel.multifunction_alarm-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index 6ae0b4997dd..ad1838b6755 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-entry] +# name: test_platform_setup_and_discovery[binary_sensor.aqi_safety-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -30,11 +30,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinco2_state', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2occo2_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][binary_sensor.aqi_safety-state] +# name: test_platform_setup_and_discovery[binary_sensor.aqi_safety-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'safety', @@ -48,7 +48,56 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-entry] +# name: test_platform_setup_and_discovery[binary_sensor.boite_aux_lettres_arriere_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.boite_aux_lettres_arriere_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.7obpyhy8scmdoorcontact_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.boite_aux_lettres_arriere_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Boîte aux lettres - arrière Door', + }), + 'context': , + 'entity_id': 'binary_sensor.boite_aux_lettres_arriere_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifer_defrost-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -79,11 +128,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'defrost', - 'unique_id': 'tuya.mock_device_iddefrost', + 'unique_id': 'tuya.ifzgvpgoodrfw2akscdefrost', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_defrost-state] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifer_defrost-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -97,7 +146,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-entry] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifer_tank_full-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -128,11 +177,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tankfull', - 'unique_id': 'tuya.mock_device_idtankfull', + 'unique_id': 'tuya.ifzgvpgoodrfw2aksctankfull', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][binary_sensor.dehumidifer_tank_full-state] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifer_tank_full-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -146,7 +195,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-entry] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifier_defrost-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -177,11 +226,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'defrost', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost', + 'unique_id': 'tuya.2myxayqtud9aqbizscdefrost', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_defrost-state] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifier_defrost-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -195,7 +244,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-entry] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifier_tank_full-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -226,11 +275,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tankfull', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull', + 'unique_id': 'tuya.2myxayqtud9aqbizsctankfull', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_tank_full-state] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifier_tank_full-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -244,7 +293,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-entry] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifier_wet-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -275,11 +324,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wet', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet', + 'unique_id': 'tuya.2myxayqtud9aqbizscwet', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][binary_sensor.dehumidifier_wet-state] +# name: test_platform_setup_and_discovery[binary_sensor.dehumidifier_wet-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -293,56 +342,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][binary_sensor.human_presence_office_occupancy-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.human_presence_office_occupancy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Occupancy', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf78687ad321a3aeb8a73mpresence_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][binary_sensor.human_presence_office_occupancy-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'occupancy', - 'friendly_name': 'Human presence Office Occupancy', - }), - 'context': , - 'entity_id': 'binary_sensor.human_presence_office_occupancy', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-entry] +# name: test_platform_setup_and_discovery[binary_sensor.door_garage_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -373,11 +373,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf5cccf9027080e2dbb9w3switch', + 'unique_id': 'tuya.bFFsO8HimyAJGIj7scmswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][binary_sensor.door_garage_door-state] +# name: test_platform_setup_and_discovery[binary_sensor.door_garage_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'door', @@ -391,7 +391,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_motion-entry] +# name: test_platform_setup_and_discovery[binary_sensor.fenetre_cuisine_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -404,7 +404,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.rat_trap_hedge_motion', + 'entity_id': 'binary_sensor.fenetre_cuisine_door', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -414,131 +414,33 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Motion', + 'original_name': 'Door', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.73486068483fda10d633pir', + 'unique_id': 'tuya.yuanswy6scmdoorcontact_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_motion-state] +# name: test_platform_setup_and_discovery[binary_sensor.fenetre_cuisine_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'rat trap hedge Motion', + 'device_class': 'door', + 'friendly_name': 'Fenêtre cuisine Door', }), 'context': , - 'entity_id': 'binary_sensor.rat_trap_hedge_motion', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_tamper-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.rat_trap_hedge_tamper', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Tamper', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.73486068483fda10d633temper_alarm', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][binary_sensor.rat_trap_hedge_tamper-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'tamper', - 'friendly_name': 'rat trap hedge Tamper', - }), - 'context': , - 'entity_id': 'binary_sensor.rat_trap_hedge_tamper', + 'entity_id': 'binary_sensor.fenetre_cuisine_door', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_motion-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Motion', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf445324326cbde7c5rg7bpir', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_motion-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'Motion sensor lidl zigbee Motion', - }), - 'context': , - 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_tamper-entry] +# name: test_platform_setup_and_discovery[binary_sensor.fenetre_cuisine_tamper-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -551,7 +453,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_tamper', + 'entity_id': 'binary_sensor.fenetre_cuisine_tamper', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -569,25 +471,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf445324326cbde7c5rg7btemper_alarm', + 'unique_id': 'tuya.yuanswy6scmtemper_alarm', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[pir_fcdjzz3s][binary_sensor.motion_sensor_lidl_zigbee_tamper-state] +# name: test_platform_setup_and_discovery[binary_sensor.fenetre_cuisine_tamper-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tamper', - 'friendly_name': 'Motion sensor lidl zigbee Tamper', + 'friendly_name': 'Fenêtre cuisine Tamper', }), 'context': , - 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_tamper', + 'entity_id': 'binary_sensor.fenetre_cuisine_tamper', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][binary_sensor.pir_outside_stairs_motion-entry] +# name: test_platform_setup_and_discovery[binary_sensor.garage_contact_sensor_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -600,7 +502,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'entity_id': 'binary_sensor.garage_contact_sensor_door', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -610,33 +512,33 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Motion', + 'original_name': 'Door', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.20401777500291cfe3a2pir', + 'unique_id': 'tuya.3uqk1csjqplf3uxqscmdoorcontact_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][binary_sensor.pir_outside_stairs_motion-state] +# name: test_platform_setup_and_discovery[binary_sensor.garage_contact_sensor_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'motion', - 'friendly_name': 'PIR outside stairs Motion', + 'device_class': 'door', + 'friendly_name': 'Garage Contact Sensor Door', }), 'context': , - 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'entity_id': 'binary_sensor.garage_contact_sensor_door', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[binary_sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -667,11 +569,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.ebb9d0eb5014f98cfboxbzgas_sensor_status', + 'unique_id': 'tuya.cwwk68dyfsh2eqi4jbqrgas_sensor_status', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][binary_sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[binary_sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'gas', @@ -685,7 +587,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wg2_nwxr8qcu4seltoro][binary_sensor.x5_zigbee_gateway_problem-entry] +# name: test_platform_setup_and_discovery[binary_sensor.gateway_problem-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -698,7 +600,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.x5_zigbee_gateway_problem', + 'entity_id': 'binary_sensor.gateway_problem', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -716,25 +618,710 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf79ca977d67322eb2o68mmaster_state', + 'unique_id': 'tuya.mpowx36sgqexmtes2gwmaster_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wg2_nwxr8qcu4seltoro][binary_sensor.x5_zigbee_gateway_problem-state] +# name: test_platform_setup_and_discovery[binary_sensor.gateway_problem-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'X5 Zigbee Gateway Problem', + 'friendly_name': 'Gateway Problem', }), 'context': , - 'entity_id': 'binary_sensor.x5_zigbee_gateway_problem', + 'entity_id': 'binary_sensor.gateway_problem', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][binary_sensor.smoke_detector_upstairs_smoke-entry] +# name: test_platform_setup_and_discovery[binary_sensor.home_gateway_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.home_gateway_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.sdq2flqkq0lblcah2gwmaster_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.home_gateway_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Home Gateway Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.home_gateway_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.human_presence_office_occupancy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.human_presence_office_occupancy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Occupancy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.kxwleaa2sphpresence_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.human_presence_office_occupancy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'occupancy', + 'friendly_name': 'Human presence Office Occupancy', + }), + 'context': , + 'entity_id': 'binary_sensor.human_presence_office_occupancy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.inondation_moisture-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.inondation_moisture', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.qwExlkou9h2USezrjswatersensor_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.inondation_moisture-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'Inondation Moisture', + }), + 'context': , + 'entity_id': 'binary_sensor.inondation_moisture', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.mesh_gateway_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mesh_gateway_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.ingdwog22gwmaster_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.mesh_gateway_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mesh-Gateway Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.mesh_gateway_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.motion_sensor_lidl_zigbee_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.s3zzjdcfrippir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.motion_sensor_lidl_zigbee_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'Motion sensor lidl zigbee Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.motion_sensor_lidl_zigbee_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.s3zzjdcfriptemper_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.motion_sensor_lidl_zigbee_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'Motion sensor lidl zigbee Tamper', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_sensor_lidl_zigbee_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.pir_outside_stairs_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.zoytcemodrn39zqwrippir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.pir_outside_stairs_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'PIR outside stairs Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.pir_outside_stairs_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rat_trap_hedge_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.rat_trap_hedge_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Motion', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.hkm4px9ohzozxma3rippir', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rat_trap_hedge_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'motion', + 'friendly_name': 'rat trap hedge Motion', + }), + 'context': , + 'entity_id': 'binary_sensor.rat_trap_hedge_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rat_trap_hedge_tamper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.rat_trap_hedge_tamper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tamper', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.hkm4px9ohzozxma3riptemper_alarm', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rat_trap_hedge_tamper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tamper', + 'friendly_name': 'rat trap hedge Tamper', + }), + 'context': , + 'entity_id': 'binary_sensor.rat_trap_hedge_tamper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rauchmelder_alexsandro_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.rauchmelder_alexsandro_smoke', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Smoke', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.o71einxvuuktuljcjbwysmoke_sensor_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rauchmelder_alexsandro_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Rauchmelder Alexsandro Smoke', + }), + 'context': , + 'entity_id': 'binary_sensor.rauchmelder_alexsandro_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rauchmelder_drucker_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.rauchmelder_drucker_smoke', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Smoke', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.t5zosev6h6wmwyrajbwysmoke_sensor_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.rauchmelder_drucker_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Rauchmelder Drucker Smoke', + }), + 'context': , + 'entity_id': 'binary_sensor.rauchmelder_drucker_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.smart_thermostats_valve-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smart_thermostats_valve', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Valve', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve', + 'unique_id': 'tuya.sb3zdertrw50bgogkwvalve_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.smart_thermostats_valve-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'smart thermostats Valve', + }), + 'context': , + 'entity_id': 'binary_sensor.smart_thermostats_valve', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.smogo_safety-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smogo_safety', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Safety', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.swhtzki3qrz5ydchjbocco_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.smogo_safety-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'safety', + 'friendly_name': 'Smogo Safety', + }), + 'context': , + 'entity_id': 'binary_sensor.smogo_safety', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.smoke_alarm_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smoke_alarm_smoke', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Smoke', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.p8xoxccrjbwysmoke_sensor_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.smoke_alarm_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'Smoke Alarm Smoke', + }), + 'context': , + 'entity_id': 'binary_sensor.smoke_alarm_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.smoke_detector_upstairs_smoke-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -765,11 +1352,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.8670375210521cf1349csmoke_sensor_status', + 'unique_id': 'tuya.jfydgffzmhjed9fgjbwysmoke_sensor_status', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][binary_sensor.smoke_detector_upstairs_smoke-state] +# name: test_platform_setup_and_discovery[binary_sensor.smoke_detector_upstairs_smoke-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'smoke', @@ -783,3 +1370,248 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[binary_sensor.soil_moisture_sensor_1_occupancy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.soil_moisture_sensor_1_occupancy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Occupancy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.oqyhsaqwsphpresence_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.soil_moisture_sensor_1_occupancy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'occupancy', + 'friendly_name': 'Soil moisture sensor #1 Occupancy', + }), + 'context': , + 'entity_id': 'binary_sensor.soil_moisture_sensor_1_occupancy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.steel_cage_door_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.steel_cage_door_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.gvxxy4jitzltz5xhscmdoorcontact_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.steel_cage_door_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Steel cage door Door', + }), + 'context': , + 'entity_id': 'binary_sensor.steel_cage_door_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.tournesol_moisture-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.tournesol_moisture', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.codvtvgtjswatersensor_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.tournesol_moisture-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'Tournesol Moisture', + }), + 'context': , + 'entity_id': 'binary_sensor.tournesol_moisture', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.wifi_smoke_alarm_smoke-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.wifi_smoke_alarm_smoke', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Smoke', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.tvgoe1s3fabebcskjbwysmoke_sensor_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.wifi_smoke_alarm_smoke-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'smoke', + 'friendly_name': 'WIFI Smoke alarm Smoke', + }), + 'context': , + 'entity_id': 'binary_sensor.wifi_smoke_alarm_smoke', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.x5_zigbee_gateway_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.x5_zigbee_gateway_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.orotles4ucq8rxwn2gwmaster_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.x5_zigbee_gateway_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'X5 Zigbee Gateway Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.x5_zigbee_gateway_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_button.ambr b/tests/components/tuya/snapshots/test_button.ambr index 61b62e124e5..6103a07d08d 100644 --- a/tests/components/tuya/snapshots/test_button.ambr +++ b/tests/components/tuya/snapshots/test_button.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_duster_cloth-entry] +# name: test_platform_setup_and_discovery[button.v20_reset_duster_cloth-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -30,11 +30,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_duster_cloth', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtreset_duster_cloth', + 'unique_id': 'tuya.zrrraytdoanz33rldsreset_duster_cloth', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_duster_cloth-state] +# name: test_platform_setup_and_discovery[button.v20_reset_duster_cloth-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'V20 Reset duster cloth', @@ -47,7 +47,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_edge_brush-entry] +# name: test_platform_setup_and_discovery[button.v20_reset_edge_brush-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -78,11 +78,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_edge_brush', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtreset_edge_brush', + 'unique_id': 'tuya.zrrraytdoanz33rldsreset_edge_brush', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_edge_brush-state] +# name: test_platform_setup_and_discovery[button.v20_reset_edge_brush-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'V20 Reset edge brush', @@ -95,7 +95,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_filter-entry] +# name: test_platform_setup_and_discovery[button.v20_reset_filter-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -126,11 +126,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_filter', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtreset_filter', + 'unique_id': 'tuya.zrrraytdoanz33rldsreset_filter', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_filter-state] +# name: test_platform_setup_and_discovery[button.v20_reset_filter-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'V20 Reset filter', @@ -143,7 +143,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_map-entry] +# name: test_platform_setup_and_discovery[button.v20_reset_map-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -174,11 +174,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_map', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtreset_map', + 'unique_id': 'tuya.zrrraytdoanz33rldsreset_map', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_map-state] +# name: test_platform_setup_and_discovery[button.v20_reset_map-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'V20 Reset map', @@ -191,7 +191,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_roll_brush-entry] +# name: test_platform_setup_and_discovery[button.v20_reset_roll_brush-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -222,11 +222,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_roll_brush', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtreset_roll_brush', + 'unique_id': 'tuya.zrrraytdoanz33rldsreset_roll_brush', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][button.v20_reset_roll_brush-state] +# name: test_platform_setup_and_discovery[button.v20_reset_roll_brush-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'V20 Reset roll brush', diff --git a/tests/components/tuya/snapshots/test_camera.ambr b/tests/components/tuya/snapshots/test_camera.ambr index e1945f03d3c..df6ea532d83 100644 --- a/tests/components/tuya/snapshots/test_camera.ambr +++ b/tests/components/tuya/snapshots/test_camera.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][camera.cam_garage-entry] +# name: test_platform_setup_and_discovery[camera.burocam-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,7 +12,7 @@ 'disabled_by': None, 'domain': 'camera', 'entity_category': None, - 'entity_id': 'camera.cam_garage', + 'entity_id': 'camera.burocam', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -30,83 +30,30 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfm', + 'unique_id': 'tuya.svjjuwykgijjedurps', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][camera.cam_garage-state] +# name: test_platform_setup_and_discovery[camera.burocam-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'access_token': '1', 'brand': 'Tuya', - 'entity_picture': '/api/camera_proxy/camera.cam_garage?token=1', - 'friendly_name': 'CAM GARAGE', - 'model_name': 'Indoor camera ', + 'entity_picture': '/api/camera_proxy/camera.burocam?token=1', + 'friendly_name': 'Bürocam', + 'model_name': 'LSC PTZ Camera', 'motion_detection': True, 'supported_features': , }), 'context': , - 'entity_id': 'camera.cam_garage', + 'entity_id': 'camera.burocam', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'idle', + 'state': 'recording', }) # --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][camera.cam_porch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'camera', - 'entity_category': None, - 'entity_id': 'camera.cam_porch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][camera.cam_porch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'access_token': '1', - 'brand': 'Tuya', - 'entity_picture': '/api/camera_proxy/camera.cam_porch?token=1', - 'friendly_name': 'CAM PORCH', - 'model_name': 'Indoor cam Pan/Tilt ', - 'supported_features': , - }), - 'context': , - 'entity_id': 'camera.cam_porch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'idle', - }) -# --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][camera.c9-entry] +# name: test_platform_setup_and_discovery[camera.c9-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -137,11 +84,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bf3f8b448bbc123e29oghf', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddsps', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][camera.c9-state] +# name: test_platform_setup_and_discovery[camera.c9-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'access_token': '1', @@ -160,3 +107,163 @@ 'state': 'recording', }) # --- +# name: test_platform_setup_and_discovery[camera.cam_garage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'camera', + 'entity_category': None, + 'entity_id': 'camera.cam_garage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.mgcpxpmovasazerdps', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[camera.cam_garage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'access_token': '1', + 'brand': 'Tuya', + 'entity_picture': '/api/camera_proxy/camera.cam_garage?token=1', + 'friendly_name': 'CAM GARAGE', + 'model_name': 'Indoor camera ', + 'motion_detection': True, + 'supported_features': , + }), + 'context': , + 'entity_id': 'camera.cam_garage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_platform_setup_and_discovery[camera.cam_porch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'camera', + 'entity_category': None, + 'entity_id': 'camera.cam_porch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrps', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[camera.cam_porch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'access_token': '1', + 'brand': 'Tuya', + 'entity_picture': '/api/camera_proxy/camera.cam_porch?token=1', + 'friendly_name': 'CAM PORCH', + 'model_name': 'Indoor cam Pan/Tilt ', + 'supported_features': , + }), + 'context': , + 'entity_id': 'camera.cam_porch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_platform_setup_and_discovery[camera.garage_camera-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'camera', + 'entity_category': None, + 'entity_id': 'camera.garage_camera', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.53fnjncm3jywuaznps', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[camera.garage_camera-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'access_token': '1', + 'brand': 'Tuya', + 'entity_picture': '/api/camera_proxy/camera.garage_camera?token=1', + 'friendly_name': 'Garage Camera', + 'model_name': 'Smart Camera ', + 'supported_features': , + }), + 'context': , + 'entity_id': 'camera.garage_camera', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'recording', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index cb535cc5c07..7687c68ad31 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-entry] +# name: test_platform_setup_and_discovery[climate.air_conditioner-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -42,11 +42,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.mock_device_id', + 'unique_id': 'tuya.mvsdcwtskkezlnw5tk', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kt_5wnlzekkstwcdsvm][climate.air_conditioner-state] +# name: test_platform_setup_and_discovery[climate.air_conditioner-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 22.0, @@ -74,7 +74,72 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-entry] +# name: test_platform_setup_and_discovery[climate.boiler_temperature_controller-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.boiler_temperature_controller', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.zgiyrxflahjowpcckw', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.boiler_temperature_controller-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 57.5, + 'friendly_name': 'Boiler Temperature Controller', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + }), + 'context': , + 'entity_id': 'climate.boiler_temperature_controller', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_platform_setup_and_discovery[climate.clima_cucina-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -120,11 +185,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bf6fc1645146455a2efrex', + 'unique_id': 'tuya.x7quooqakw', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_aqoouq7x][climate.clima_cucina-state] +# name: test_platform_setup_and_discovery[climate.clima_cucina-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 27.0, @@ -155,7 +220,500 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-entry] +# name: test_platform_setup_and_discovery[climate.kabinet-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 95.0, + 'min_temp': 5.0, + 'preset_modes': list([ + 'program', + ]), + 'target_temp_step': 0.5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.kabinet', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.dn7cjik6kw', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.kabinet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 19.5, + 'friendly_name': 'Кабінет', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 95.0, + 'min_temp': 5.0, + 'preset_mode': None, + 'preset_modes': list([ + 'program', + ]), + 'supported_features': , + 'target_temp_step': 0.5, + 'temperature': 21.5, + }), + 'context': , + 'entity_id': 'climate.kabinet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_platform_setup_and_discovery[climate.master_bedroom_ac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + , + , + ]), + 'max_temp': 88.0, + 'min_temp': 16.0, + 'target_temp_step': 0.5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.master_bedroom_ac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.g1fmm26qhhrimmbitk', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.master_bedroom_ac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 0, + 'current_temperature': 26.0, + 'friendly_name': 'Master Bedroom AC', + 'hvac_modes': list([ + , + , + , + , + , + , + ]), + 'max_temp': 88.0, + 'min_temp': 16.0, + 'supported_features': , + 'target_temp_step': 0.5, + 'temperature': 75.0, + }), + 'context': , + 'entity_id': 'climate.master_bedroom_ac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_platform_setup_and_discovery[climate.mr_pure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + ]), + 'max_temp': 35, + 'min_temp': 7, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.mr_pure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.gnqwzcph94wj2sl5nq', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.mr_pure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': None, + 'friendly_name': 'Mr. Pure', + 'hvac_modes': list([ + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + 'target_temp_step': 1.0, + }), + 'context': , + 'entity_id': 'climate.mr_pure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[climate.polotentsosushitel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 40.0, + 'min_temp': 5.0, + 'preset_modes': list([ + 'holiday', + ]), + 'target_temp_step': 0.5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.polotentsosushitel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.53apxfah2qoxb1cgkw', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.polotentsosushitel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 25.3, + 'friendly_name': 'Полотенцосушитель', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 40.0, + 'min_temp': 5.0, + 'preset_mode': 'hold', + 'preset_modes': list([ + 'holiday', + ]), + 'supported_features': , + 'target_temp_step': 0.5, + 'temperature': 5.0, + }), + 'context': , + 'entity_id': 'climate.polotentsosushitel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[climate.smart_thermostats-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 90.0, + 'min_temp': 5.0, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.smart_thermostats', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.sb3zdertrw50bgogkw', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.smart_thermostats-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 21.5, + 'friendly_name': 'smart thermostats', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 90.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 12.0, + }), + 'context': , + 'entity_id': 'climate.smart_thermostats', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[climate.sove-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + '1', + '2', + ]), + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 86.0, + 'min_temp': 16.0, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.sove', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.dt4whlrosmnldadvtk', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.sove-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 24.0, + 'fan_mode': 2, + 'fan_modes': list([ + '1', + '2', + ]), + 'friendly_name': 'Sove', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 86.0, + 'min_temp': 16.0, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 16.0, + }), + 'context': , + 'entity_id': 'climate.sove', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[climate.term_prizemi-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 70.0, + 'min_temp': 0.5, + 'target_temp_step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.term_prizemi', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.jm2fsqtzuhqtbo5ykw', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.term_prizemi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 23.0, + 'friendly_name': 'Term - Prizemi', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 70.0, + 'min_temp': 0.5, + 'supported_features': , + 'target_temp_step': 0.1, + 'temperature': 23.0, + }), + 'context': , + 'entity_id': 'climate.term_prizemi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_platform_setup_and_discovery[climate.wifi_smart_gas_boiler_thermostat-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -194,11 +752,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bfb45cb8a9452fba66lexg', + 'unique_id': 'tuya.j6mn1t4ut5end6ifkw', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][climate.wifi_smart_gas_boiler_thermostat-state] +# name: test_platform_setup_and_discovery[climate.wifi_smart_gas_boiler_thermostat-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 24.9, diff --git a/tests/components/tuya/snapshots/test_cover.ambr b/tests/components/tuya/snapshots/test_cover.ambr index 0c556a90494..f18c96596b1 100644 --- a/tests/components/tuya/snapshots/test_cover.ambr +++ b/tests/components/tuya/snapshots/test_cover.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_3r8gc33pnqsxfe1g][cover.lounge_dark_blind_curtain-entry] +# name: test_platform_setup_and_discovery[cover.bedroom_blinds_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,7 +12,7 @@ 'disabled_by': None, 'domain': 'cover', 'entity_category': None, - 'entity_id': 'cover.lounge_dark_blind_curtain', + 'entity_id': 'cover.bedroom_blinds_curtain', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -30,27 +30,27 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'curtain', - 'unique_id': 'tuya.mocked_device_idcontrol', + 'unique_id': 'tuya.thdfxdqqlccontrol', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_3r8gc33pnqsxfe1g][cover.lounge_dark_blind_curtain-state] +# name: test_platform_setup_and_discovery[cover.bedroom_blinds_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_position': 100, + 'current_position': 0, 'device_class': 'curtain', - 'friendly_name': 'Lounge Dark Blind Curtain', + 'friendly_name': 'bedroom blinds Curtain', 'supported_features': , }), 'context': , - 'entity_id': 'cover.lounge_dark_blind_curtain', + 'entity_id': 'cover.bedroom_blinds_curtain', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'open', + 'state': 'closed', }) # --- -# name: test_platform_setup_and_discovery[cl_cpbo62rn][cover.blinds_curtain-entry] +# name: test_platform_setup_and_discovery[cover.blinds_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -81,11 +81,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'curtain', - 'unique_id': 'tuya.bf216113c71bf01a18jtl0control', + 'unique_id': 'tuya.nr26obpclccontrol', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_cpbo62rn][cover.blinds_curtain-state] +# name: test_platform_setup_and_discovery[cover.blinds_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 36, @@ -101,7 +101,57 @@ 'state': 'open', }) # --- -# name: test_platform_setup_and_discovery[cl_ebt12ypvexnixvtf][cover.kitchen_blinds_blind-entry] +# name: test_platform_setup_and_discovery[cover.garage_door_door_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.garage_door_door_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_door', + 'unique_id': 'tuya.q3iie9vjd4wfqyy1qzkmkcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cover.garage_door_door_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Garage door Door 1', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.garage_door_door_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_platform_setup_and_discovery[cover.kitchen_blinds_blind-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -132,11 +182,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'blind', - 'unique_id': 'tuya.mocked_device_idswitch_1', + 'unique_id': 'tuya.ftvxinxevpy21tbelcswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_ebt12ypvexnixvtf][cover.kitchen_blinds_blind-state] +# name: test_platform_setup_and_discovery[cover.kitchen_blinds_blind-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 100, @@ -152,58 +202,7 @@ 'state': 'open', }) # --- -# name: test_platform_setup_and_discovery[cl_qqdxfdht][cover.bedroom_blinds_curtain-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.bedroom_blinds_curtain', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Curtain', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': 'curtain', - 'unique_id': 'tuya.bfb9c4958fd06d141djpqacontrol', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cl_qqdxfdht][cover.bedroom_blinds_curtain-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'current_position': 0, - 'device_class': 'curtain', - 'friendly_name': 'bedroom blinds Curtain', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.bedroom_blinds_curtain', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'closed', - }) -# --- -# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-entry] +# name: test_platform_setup_and_discovery[cover.kitchen_blinds_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -234,11 +233,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'curtain', - 'unique_id': 'tuya.zah67ekdcontrol', + 'unique_id': 'tuya.dke76hazlccontrol', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_zah67ekd][cover.kitchen_blinds_curtain-state] +# name: test_platform_setup_and_discovery[cover.kitchen_blinds_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 48, @@ -254,7 +253,109 @@ 'state': 'open', }) # --- -# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-entry] +# name: test_platform_setup_and_discovery[cover.lounge_dark_blind_curtain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.lounge_dark_blind_curtain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Curtain', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'curtain', + 'unique_id': 'tuya.g1efxsqnp33cg8r3lccontrol', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cover.lounge_dark_blind_curtain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'device_class': 'curtain', + 'friendly_name': 'Lounge Dark Blind Curtain', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.lounge_dark_blind_curtain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_platform_setup_and_discovery[cover.persiana_do_quarto_curtain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.persiana_do_quarto_curtain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Curtain', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'curtain', + 'unique_id': 'tuya.ikbbdbnqsd70pc1glccontrol', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cover.persiana_do_quarto_curtain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'device_class': 'curtain', + 'friendly_name': 'Persiana do Quarto Curtain', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.persiana_do_quarto_curtain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_platform_setup_and_discovery[cover.tapparelle_studio_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -285,11 +386,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'curtain', - 'unique_id': 'tuya.bf1fa053e0ba4e002c6we8control', + 'unique_id': 'tuya.2w46jyhngklccontrol', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[clkg_nhyj64w2][cover.tapparelle_studio_curtain-state] +# name: test_platform_setup_and_discovery[cover.tapparelle_studio_curtain-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_position': 0, @@ -305,3 +406,54 @@ 'state': 'closed', }) # --- +# name: test_platform_setup_and_discovery[cover.vividstorm_screen_curtain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.vividstorm_screen_curtain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Curtain', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'curtain', + 'unique_id': 'tuya.4hbnivc4w2rsw966lccontrol', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cover.vividstorm_screen_curtain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'device_class': 'curtain', + 'friendly_name': 'VIVIDSTORM SCREEN Curtain', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.vividstorm_screen_curtain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_diagnostics.ambr b/tests/components/tuya/snapshots/test_diagnostics.ambr index 93cc0cd0b6d..33248655d31 100644 --- a/tests/components/tuya/snapshots/test_diagnostics.ambr +++ b/tests/components/tuya/snapshots/test_diagnostics.ambr @@ -59,7 +59,7 @@ 'name': 'Gas sensor', 'name_by_user': None, }), - 'id': 'ebb9d0eb5014f98cfboxbz', + 'id': 'cwwk68dyfsh2eqi4jbqr', 'mqtt_connected': True, 'name': 'Gas sensor', 'online': True, @@ -147,7 +147,7 @@ 'name': 'Gas sensor', 'name_by_user': None, }), - 'id': 'ebb9d0eb5014f98cfboxbz', + 'id': 'cwwk68dyfsh2eqi4jbqr', 'name': 'Gas sensor', 'online': True, 'product_id': '4iqe2hsfyd86kwwc', diff --git a/tests/components/tuya/snapshots/test_event.ambr b/tests/components/tuya/snapshots/test_event.ambr index ea19ff486da..ce7c1cf67de 100644 --- a/tests/components/tuya/snapshots/test_event.ambr +++ b/tests/components/tuya/snapshots/test_event.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-entry] +# name: test_platform_setup_and_discovery[event.bathroom_smart_switch_button_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -35,11 +35,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'numbered_button', - 'unique_id': 'tuya.mocked_device_idswitch_mode1', + 'unique_id': 'tuya.fvywp3b5mu4zay8lgkxwswitch_mode1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_1-state] +# name: test_platform_setup_and_discovery[event.bathroom_smart_switch_button_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', @@ -58,7 +58,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-entry] +# name: test_platform_setup_and_discovery[event.bathroom_smart_switch_button_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -94,11 +94,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'numbered_button', - 'unique_id': 'tuya.mocked_device_idswitch_mode2', + 'unique_id': 'tuya.fvywp3b5mu4zay8lgkxwswitch_mode2', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][event.bathroom_smart_switch_button_2-state] +# name: test_platform_setup_and_discovery[event.bathroom_smart_switch_button_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', @@ -117,3 +117,64 @@ 'state': 'unknown', }) # --- +# name: test_platform_setup_and_discovery[event.bouton_tempo_exterieur_button_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'click', + 'double_click', + 'press', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.bouton_tempo_exterieur_button_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Button 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'numbered_button', + 'unique_id': 'tuya.g5uso5ajgkxwswitch_mode1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[event.bouton_tempo_exterieur_button_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'click', + 'double_click', + 'press', + ]), + 'friendly_name': 'Bouton tempo extérieur Button 1', + }), + 'context': , + 'entity_id': 'event.bouton_tempo_exterieur_button_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_fan.ambr b/tests/components/tuya/snapshots/test_fan.ambr index 7532023860b..f2b615ec269 100644 --- a/tests/components/tuya/snapshots/test_fan.ambr +++ b/tests/components/tuya/snapshots/test_fan.ambr @@ -1,5 +1,62 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-entry] +# name: test_platform_setup_and_discovery[fan.bree-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + 'sleep', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.bree', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.ppgdpsq1xaxlyzryjk', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fan.bree-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bree', + 'preset_mode': 'normal', + 'preset_modes': list([ + 'sleep', + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.bree', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[fan.ceiling_fan_light_v2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -15,7 +72,7 @@ 'disabled_by': None, 'domain': 'fan', 'entity_category': None, - 'entity_id': 'fan.dehumidifer', + 'entity_id': 'fan.ceiling_fan_light_v2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,179 +88,33 @@ 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.mock_device_id', + 'unique_id': 'tuya.6wxksqu35c61sce9dsf', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][fan.dehumidifer-state] +# name: test_platform_setup_and_discovery[fan.ceiling_fan_light_v2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifer', + 'direction': 'forward', + 'friendly_name': 'ceiling fan/Light v2', + 'percentage': 20, + 'percentage_step': 1.0, + 'preset_mode': None, 'preset_modes': list([ ]), - 'supported_features': , + 'supported_features': , }), 'context': , - 'entity_id': 'fan.dehumidifer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][fan.dryfix-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'fan', - 'entity_category': None, - 'entity_id': 'fan.dryfix', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.28403630e8db84b7a963', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][fan.dryfix-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'DryFix', - 'supported_features': , - }), - 'context': , - 'entity_id': 'fan.dryfix', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'fan', - 'entity_category': None, - 'entity_id': 'fan.dehumidifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.mock_device_id', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][fan.dehumidifier-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier ', - 'supported_features': , - }), - 'context': , - 'entity_id': 'fan.dehumidifier', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'fan', - 'entity_category': None, - 'entity_id': 'fan.dehumidifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'tuya.bf3fce6af592f12df3gbgq', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][fan.dehumidifier-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier', - 'supported_features': , - }), - 'context': , - 'entity_id': 'fan.dehumidifier', + 'entity_id': 'fan.ceiling_fan_light_v2', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][fan.ceiling_fan_with_light-entry] +# name: test_platform_setup_and_discovery[fan.ceiling_fan_with_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -240,11 +151,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.XXX', + 'unique_id': 'tuya.ijzjlqwmv1blwe0gsf', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][fan.ceiling_fan_with_light-state] +# name: test_platform_setup_and_discovery[fan.ceiling_fan_with_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'direction': 'reverse', @@ -267,12 +178,14 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[fs_ibytpo6fpnugft1c][fan.ventilador_cama-entry] +# name: test_platform_setup_and_discovery[fan.dehumidifer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ + 'preset_modes': list([ + ]), }), 'config_entry_id': , 'config_subentry_id': , @@ -281,7 +194,7 @@ 'disabled_by': None, 'domain': 'fan', 'entity_category': None, - 'entity_id': 'fan.ventilador_cama', + 'entity_id': 'fan.dehumidifer', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -297,27 +210,79 @@ 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': 0, + 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.10706550a4e57c88b93a', + 'unique_id': 'tuya.ifzgvpgoodrfw2aksc', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[fs_ibytpo6fpnugft1c][fan.ventilador_cama-state] +# name: test_platform_setup_and_discovery[fan.dehumidifer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ventilador Cama', - 'supported_features': , + 'friendly_name': 'Dehumidifer', + 'preset_modes': list([ + ]), + 'supported_features': , }), 'context': , - 'entity_id': 'fan.ventilador_cama', + 'entity_id': 'fan.dehumidifer', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][fan.hl400-entry] +# name: test_platform_setup_and_discovery[fan.dehumidifier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.dehumidifier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.2myxayqtud9aqbizsc', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fan.dehumidifier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.dehumidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[fan.hl400-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -351,11 +316,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.152027113c6105cce49c', + 'unique_id': 'tuya.zfHZQ7tZUBxAWjACjk', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][fan.hl400-state] +# name: test_platform_setup_and_discovery[fan.hl400-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'HL400', @@ -374,13 +339,65 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-entry] +# name: test_platform_setup_and_discovery[fan.ion1000pro-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.ion1000pro', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.owozxdzgbibizu4sjk', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fan.ion1000pro-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.ion1000pro', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[fan.kalado_air_purifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'preset_modes': list([ + 'manual', + 'auto', 'sleep', ]), }), @@ -391,7 +408,7 @@ 'disabled_by': None, 'domain': 'fan', 'entity_category': None, - 'entity_id': 'fan.bree', + 'entity_id': 'fan.kalado_air_purifier', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -409,29 +426,31 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.CENSORED', + 'unique_id': 'tuya.yo2karkjuhzztxsfjk', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][fan.bree-state] +# name: test_platform_setup_and_discovery[fan.kalado_air_purifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bree', - 'preset_mode': 'normal', + 'friendly_name': 'Kalado Air Purifier', + 'preset_mode': 'auto', 'preset_modes': list([ + 'manual', + 'auto', 'sleep', ]), 'supported_features': , }), 'context': , - 'entity_id': 'fan.bree', + 'entity_id': 'fan.kalado_air_purifier', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-entry] +# name: test_platform_setup_and_discovery[fan.tower_fan_ca_407g_smart-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -468,11 +487,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.mock_device_id', + 'unique_id': 'tuya.lflvu8cazha8af9jsk', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][fan.tower_fan_ca_407g_smart-state] +# name: test_platform_setup_and_discovery[fan.tower_fan_ca_407g_smart-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tower Fan CA-407G Smart', diff --git a/tests/components/tuya/snapshots/test_humidifier.ambr b/tests/components/tuya/snapshots/test_humidifier.ambr index 33034e3f6e7..46535810d7d 100644 --- a/tests/components/tuya/snapshots/test_humidifier.ambr +++ b/tests/components/tuya/snapshots/test_humidifier.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-entry] +# name: test_platform_setup_and_discovery[humidifier.dehumidifer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -33,11 +33,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.mock_device_idswitch', + 'unique_id': 'tuya.ifzgvpgoodrfw2akscswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][humidifier.dehumidifer-state] +# name: test_platform_setup_and_discovery[humidifier.dehumidifer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'dehumidifier', @@ -54,117 +54,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][humidifier.dryfix-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_humidity': 100, - 'min_humidity': 0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'humidifier', - 'entity_category': None, - 'entity_id': 'humidifier.dryfix', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.28403630e8db84b7a963switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_qhxmvae667uap4zh][humidifier.dryfix-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'dehumidifier', - 'friendly_name': 'DryFix', - 'max_humidity': 100, - 'min_humidity': 0, - 'supported_features': , - }), - 'context': , - 'entity_id': 'humidifier.dryfix', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_humidity': 100, - 'min_humidity': 0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'humidifier', - 'entity_category': None, - 'entity_id': 'humidifier.dehumidifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.mock_device_idswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_vmxuxszzjwp5smli][humidifier.dehumidifier-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'dehumidifier', - 'friendly_name': 'Dehumidifier ', - 'max_humidity': 100, - 'min_humidity': 0, - 'supported_features': , - }), - 'context': , - 'entity_id': 'humidifier.dehumidifier', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-entry] +# name: test_platform_setup_and_discovery[humidifier.dehumidifier-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -198,11 +88,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqswitch', + 'unique_id': 'tuya.2myxayqtud9aqbizscswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][humidifier.dehumidifier-state] +# name: test_platform_setup_and_discovery[humidifier.dehumidifier-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_humidity': 47, diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 83548abf0c3..a70d38c6fbc 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -1,34 +1,6790 @@ # serializer version: 1 -# name: test_unsupported_device[ydkt_jevroj5aguwdbs2e] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'tuya', - 'mock_device_id', - ), - }), - 'labels': set({ - }), - 'manufacturer': 'Tuya', - 'model': 'DOLCECLIMA 10 HP WIFI (unsupported)', - 'model_id': 'jevroj5aguwdbs2e', - 'name': 'DOLCECLIMA 10 HP WIFI', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_device_registry[2k8wyjo7iidkohuczc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ }), - ]) + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '2k8wyjo7iidkohuczc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug-EU', + 'model_id': 'cuhokdii7ojyw8k2', + 'name': 'Buitenverlichting', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[2myxayqtud9aqbizsc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '2myxayqtud9aqbizsc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Arete® Two 12L Dehumidifier/Air Purifier', + 'model_id': 'zibqa9dutqyaxym2', + 'name': 'Dehumidifier', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[2pxfek1jjrtctiyglam] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '2pxfek1jjrtctiyglam', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Multifunction alarm', + 'model_id': 'gyitctrjj1kefxp2', + 'name': 'Multifunction alarm', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[2w46jyhngklc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '2w46jyhngklc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Curtain switch', + 'model_id': 'nhyj64w2', + 'name': 'Tapparelle studio', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[2x473nefusdo7af6zc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '2x473nefusdo7af6zc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '5GHz plug', + 'model_id': '6fa7odsufen374x2', + 'name': 'Office', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[3d4yosotwk27nqxvzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '3d4yosotwk27nqxvzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug+', + 'model_id': 'vxqn72kwtosoy4d3', + 'name': 'Garage Socket', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[3phkffywh5nnlj5vbdnz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '3phkffywh5nnlj5vbdnz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Meter', + 'model_id': 'v5jlnn5hwyffkhp3', + 'name': 'Production', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[3uqk1csjqplf3uxqscm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '3uqk1csjqplf3uxqscm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Contact Sensor', + 'model_id': 'qxu3flpqjsc1kqu3', + 'name': 'Garage Contact Sensor', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[49m7h9lh3t8pq6ftzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '49m7h9lh3t8pq6ftzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'smart meter with CT-2', + 'model_id': 'tf6qp8t3hl9h7m94', + 'name': 'Consommation', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[4fO1qIzYbcdMUHqAjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '4fO1qIzYbcdMUHqAjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Bulb', + 'model_id': 'AqHUMdcbYzIq1Of4', + 'name': 'Landing', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[4hbnivc4w2rsw966lc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '4hbnivc4w2rsw966lc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'VIVIDSTORM SCREEN', + 'model_id': '669wsr2w4cvinbh4', + 'name': 'VIVIDSTORM SCREEN', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[4pa1uobdjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '4pa1uobdjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'atmosphere', + 'model_id': 'dbou1ap4', + 'name': 'Lumy Garage', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[4q5c2am8n1bwb6bszc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '4q5c2am8n1bwb6bszc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WIFI 插座', + 'model_id': 'sb6bwb1n8ma2c5q4', + 'name': 'Socket4', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[51tdkcsamisw9ukycp] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '51tdkcsamisw9ukycp', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Konyks Priska USB', + 'model_id': 'yku9wsimasckdt15', + 'name': 'Framboisier', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[53apxfah2qoxb1cgkw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '53apxfah2qoxb1cgkw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ET-44W', + 'model_id': 'gc1bxoq2hafxpa35', + 'name': 'Полотенцосушитель', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[53fnjncm3jywuaznps] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '53fnjncm3jywuaznps', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Camera ', + 'model_id': 'nzauwyj3mcnjnf35', + 'name': 'Garage Camera', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[5gfyvvg48bsxbbnjzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '5gfyvvg48bsxbbnjzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Plug Base 6210HA', + 'model_id': 'jnbbxsb84gvvyfg5', + 'name': 'Bathroom Fan', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[63cninaczt9dwo7v2gw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '63cninaczt9dwo7v2gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Gateway (unsupported)', + 'model_id': 'v7owd9tzcaninc36', + 'name': 'Gateway2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[69dth3rxgcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '69dth3rxgcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Temperature Humidity Sensor', + 'model_id': 'xr3htd96', + 'name': 'Humy toilettes RDC', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[6ffyxwrjsuydxhqrqkynw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6ffyxwrjsuydxhqrqkynw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart IR (unsupported)', + 'model_id': 'rqhxdyusjrwxyff6', + 'name': 'Smart IR', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[6gsqieoh1yzjvxlnjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6gsqieoh1yzjvxlnjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LED SMART', + 'model_id': 'nlxvjzy1hoeiqsg6', + 'name': 'hall 💡 ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[6h8boeqxorpsmtj] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6h8boeqxorpsmtj', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'S1-TY-BLE-PRO (unsupported)', + 'model_id': 'xqeob8h6', + 'name': 'S1-TY-BLE-PRO 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[6o148laaosbf0g4djd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6o148laaosbf0g4djd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'A60 GOLD', + 'model_id': 'd4g0fbsoaal841o6', + 'name': 'WC D1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[6tbtkuv3tal1aesfjxq] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6tbtkuv3tal1aesfjxq', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'BR 7-in-1 WLAN Wetterstation Anthrazit', + 'model_id': 'fsea1lat3vuktbt6', + 'name': 'BR 7-in-1 WLAN Wetterstation Anthrazit', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[6wxksqu35c61sce9dsf] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6wxksqu35c61sce9dsf', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ceiling fan/Light v2', + 'model_id': '9ecs16c53uqskxw6', + 'name': 'ceiling fan/Light v2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[73ov8i8iedtylkzrqzkfs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '73ov8i8iedtylkzrqzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Water Timer', + 'model_id': 'rzklytdei8i8vo37', + 'name': 'balkonbewässerung', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[7axah58vfydd8cphjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '7axah58vfydd8cphjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'RGB Smart Plug', + 'model_id': 'hpc8ddyfv85haxa7', + 'name': 'Garage', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[7jxnjpiltmj2zyaijd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '7jxnjpiltmj2zyaijd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LED Strip RGB+W', + 'model_id': 'iayz2jmtlipjnxj7', + 'name': 'LED Porch 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[7obpyhy8scm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '7obpyhy8scm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Door Sensor', + 'model_id': '8yhypbo7', + 'name': 'Boîte aux lettres - arrière', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[7zogt3pcwhxhu8upqdt] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '7zogt3pcwhxhu8upqdt', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug +', + 'model_id': 'pu8uhxhwcp3tgoz7', + 'name': 'Socket3', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[86kdcut3hiqqddlijd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '86kdcut3hiqqddlijd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LED SMART', + 'model_id': 'ilddqqih3tucdk68', + 'name': 'Ieskas', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[87yarxyp23ap1vazjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '87yarxyp23ap1vazjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Ceiling Light RGBTW', + 'model_id': 'zav1pa32pyxray78', + 'name': 'Gengske 💡 ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[97k3pwirjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '97k3pwirjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'atmosphere', + 'model_id': 'riwp3k79', + 'name': 'LED KEUKEN 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[9oh1h1uyalfykgg4bdnz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '9oh1h1uyalfykgg4bdnz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'XOCA-DAC212XC V2-S1', + 'model_id': '4ggkyflayu1h1ho9', + 'name': 'XOCA-DAC212XC V2-S1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[9wlo8cpzprhiclrkgcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '9wlo8cpzprhiclrkgcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'IFS-STD002', + 'model_id': 'krlcihrpzpc8olw9', + 'name': 'IFS-STD002', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[AUTwCwqDY9EjlQSocm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'AUTwCwqDY9EjlQSocm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Door Sensor', + 'model_id': 'oSQljE9YDqwCwTUA', + 'name': 'Kippenluik', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[CyD4ctKVrAFSSXSbjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'CyD4ctKVrAFSSXSbjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Dimmer switch', + 'model_id': 'bSXSSFArVKtc4DyC', + 'name': 'bedroom', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[HzsAAAKFLPABVi8nzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'HzsAAAKFLPABVi8nzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Socket', + 'model_id': 'n8iVBAPLFKAAAszH', + 'name': 'Steckdose 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[LJ9zTFQTfMgsG2Ahzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'LJ9zTFQTfMgsG2Ahzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Mini Smart Socket', + 'model_id': 'hA2GsgMfTQFTz9JL', + 'name': 'Spot 4', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[LS6FfVBVU1vzBRBHzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'LS6FfVBVU1vzBRBHzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Rewireable Plug 6930HA', + 'model_id': 'HBRBzv1UVBVfF6SL', + 'name': 'Rewireable Plug 6930HA', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[NVjuXIQ6QH9eZLHCzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'NVjuXIQ6QH9eZLHCzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug', + 'model_id': 'CHLZe9HQ6QIXujVN', + 'name': 'schuur', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[O8QpxJwdme33sqn4gk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'O8QpxJwdme33sqn4gk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'SWITCH1', + 'model_id': '4nqs33emdwJxpQ8O', + 'name': 'office lights', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[VA4QyBNZHkJ2Xa4hjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'VA4QyBNZHkJ2Xa4hjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Dimmer Switch', + 'model_id': 'h4aX2JkHZNByQ4AV', + 'name': 'Entry Stairs', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[YQLkAe7nyyAxXHiAzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'YQLkAe7nyyAxXHiAzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Mini Smart Plug', + 'model_id': 'AiHXxAyyn7eAkLQY', + 'name': 'Solar Heater Pump', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ZgXzZULP6dDp4Atvgcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ZgXzZULP6dDp4Atvgcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Temperature and humidity sensor', + 'model_id': 'vtA4pDd6PLUZzXgZ', + 'name': 'Humy bain', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[a3qtb7pulkcc6jdjqld] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'a3qtb7pulkcc6jdjqld', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WiFi Din Rail Switch with metering', + 'model_id': 'jdj6ccklup7btq3a', + 'name': 'Eau Chaude', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[a4zeazrz1ata9mbggk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'a4zeazrz1ata9mbggk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Valve', + 'model_id': 'gbm9ata1zrzaez4a', + 'name': 'QT-Switch', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[aa99hccfnzvypr3zjsywc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'aa99hccfnzvypr3zjsywc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': 'z3rpyvznfcch99aa', + 'name': 'PIXI Smart Drinking Fountain', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[aje5kxgmhhxdihqizc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'aje5kxgmhhxdihqizc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug', + 'model_id': 'iqhidxhhmgxk5eja', + 'name': 'Powerplug 5', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ajkdo1bm2rcmpuufjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ajkdo1bm2rcmpuufjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ST64 Clear', + 'model_id': 'fuupmcr2mb1odkja', + 'name': 'Slaapkamer', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[aoyweq8xbx7qfndijd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'aoyweq8xbx7qfndijd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Lamp', + 'model_id': 'idnfq7xbx8qewyoa', + 'name': 'AB1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ase6htln9tdni2sijxq] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ase6htln9tdni2sijxq', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'T & H Sensor with external probe', + 'model_id': 'is2indt9nlth6esa', + 'name': 'Frysen', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[b6e05dfy4qhpgea1qdt] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'b6e05dfy4qhpgea1qdt', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '1-433', + 'model_id': '1aegphq4yfd50e6b', + 'name': 'jardin Fraises', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bFFsO8HimyAJGIj7scm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bFFsO8HimyAJGIj7scm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Door Sensor', + 'model_id': '7jIGJAymiH8OsFFb', + 'name': 'Door Garage ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bak2crzmabancwqvjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bak2crzmabancwqvjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Light Strip-RGBCW ', + 'model_id': 'vqwcnabamzrc2kab', + 'name': 'Strip 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bcyciyhhu1g2gk9rqld] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bcyciyhhu1g2gk9rqld', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Breaker ', + 'model_id': 'r9kg2g1uhhyicycb', + 'name': 'P1 Energia Elettrica', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bescacsciyam3aouqdt] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bescacsciyam3aouqdt', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Curtain switch (unsupported)', + 'model_id': 'uoa3mayicscacseb', + 'name': 'Living room left', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bfpewgk8r6fhmissdyzb] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bfpewgk8r6fhmissdyzb', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'BlissRadia (unsupported)', + 'model_id': 'ssimhf6r8kgwepfb', + 'name': 'BlissRadia ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bgnj6bafrdgb1xmajd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bgnj6bafrdgb1xmajd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'A60 Clear', + 'model_id': 'amx1bgdrfab6jngb', + 'name': 'Lumy Hall', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bl5cuqxnqzkfs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bl5cuqxnqzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Water Timer', + 'model_id': 'nxquc5lb', + 'name': 'Smart Water Timer', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[buzituffc13pgb1jjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'buzituffc13pgb1jjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LSC Smart Ceiling Light', + 'model_id': 'j1bgp31cffutizub', + 'name': 'Ceiling Portal', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[bxfkpxjgux2fgwnazc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'bxfkpxjgux2fgwnazc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Socket ', + 'model_id': 'anwgf2xugjxpkfxb', + 'name': 'Security Light', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[c1tfgunpf6optybisf] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'c1tfgunpf6optybisf', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Tower bladeless fan (unsupported)', + 'model_id': 'ibytpo6fpnugft1c', + 'name': 'Ventilador Cama', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[cijerqyssiwrf7deqzkfs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'cijerqyssiwrf7deqzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '接HA水阀', + 'model_id': 'ed7frwissyqrejic', + 'name': '接HA水阀', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[cju47ovcbeuapei2zc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'cju47ovcbeuapei2zc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Aubess Smart\xa0Socket 20A/EM', + 'model_id': '2iepauebcvo74ujc', + 'name': 'Aubess Cooker', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[codvtvgtjs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'codvtvgtjs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Rain sensor', + 'model_id': 'tgvtvdoc', + 'name': 'Tournesol', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[couukaypjdnyt] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'couukaypjdnyt', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Solar flood light App panel', + 'model_id': 'pyakuuoc', + 'name': 'Solar zijpad', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[cq4hzlrnqn4qi0mqzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'cq4hzlrnqn4qi0mqzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart plug', + 'model_id': 'qm0iq4nqnrlzh4qc', + 'name': 'Elivco Kitchen Socket', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[cwwk68dyfsh2eqi4jbqr] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'cwwk68dyfsh2eqi4jbqr', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Gas sensor', + 'model_id': '4iqe2hsfyd86kwwc', + 'name': 'Gas sensor', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[dke76hazlc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'dke76hazlc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'AM43拉绳电机-Zigbee', + 'model_id': 'zah67ekd', + 'name': 'Kitchen Blinds', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[dn7cjik6kw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'dn7cjik6kw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Thermostat Tervix Pro Line ZigBee color', + 'model_id': '6kijc7nd', + 'name': 'Кабінет', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[dt4whlrosmnldadvtk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'dt4whlrosmnldadvtk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'YFA-05C', + 'model_id': 'vdadlnmsorlhw4td', + 'name': 'Sove', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[dvdtmcoil5yopaljjzm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'dvdtmcoil5yopaljjzm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ISV-100W2.0 (unsupported)', + 'model_id': 'jlapoy5liocmtdvd', + 'name': 'ISV-100W2.0', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[e2sbdwuga5jorvejtkdy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'e2sbdwuga5jorvejtkdy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'DOLCECLIMA 10 HP WIFI (unsupported)', + 'model_id': 'jevroj5aguwdbs2e', + 'name': 'DOLCECLIMA 10 HP WIFI', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[eway2kw92ncuecarzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'eway2kw92ncuecarzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Inline Switch 6000HA', + 'model_id': 'raceucn29wk2yawe', + 'name': 'Bathroom Mirror', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[fasvixqysw1lxvjprd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'fasvixqysw1lxvjprd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Sunbeam Bedding', + 'model_id': 'pjvxl1wsyqxivsaf', + 'name': 'Sunbeam Bedding', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[fbya6s6rhaoyvl8hqgcwy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'fbya6s6rhaoyvl8hqgcwy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Tank A Level', + 'model_id': 'h8lvyoahr6s6aybf', + 'name': 'Rainwater Tank Level', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[fcdadqsiax2gvnt0qld] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'fcdadqsiax2gvnt0qld', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '一路带计量磁保持通断器', + 'model_id': '0tnvg2xaisqdadcf', + 'name': '一路带计量磁保持通断器', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[fjdyw5ld2f5f5ddsps] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'fjdyw5ld2f5f5ddsps', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Security Camera', + 'model_id': 'sdd5f5f2dl5wydjf', + 'name': 'C9', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ftvxinxevpy21tbelc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ftvxinxevpy21tbelc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Blinds', + 'model_id': 'ebt12ypvexnixvtf', + 'name': 'Kitchen Blinds', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[fvywp3b5mu4zay8lgkxw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'fvywp3b5mu4zay8lgkxw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Wireless Switch', + 'model_id': 'l8yaz4um5b3pwyvf', + 'name': 'Bathroom Smart Switch', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[g0edqq0wzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'g0edqq0wzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart plug', + 'model_id': 'w0qqde0g', + 'name': 'Lave linge', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[g1efxsqnp33cg8r3lc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'g1efxsqnp33cg8r3lc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Blinds Controller', + 'model_id': '3r8gc33pnqsxfe1g', + 'name': 'Lounge Dark Blind', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[g1fmm26qhhrimmbitk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'g1fmm26qhhrimmbitk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'T platform model-USB ', + 'model_id': 'ibmmirhhq62mmf1g', + 'name': 'Master Bedroom AC', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[g5uso5ajgkxw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'g5uso5ajgkxw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ZC-YED-一键无线开关', + 'model_id': 'ja5osu5g', + 'name': 'Bouton tempo extérieur', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[g7af6lrt4miugbstcp] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'g7af6lrt4miugbstcp', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Power Strip', + 'model_id': 'tsbguim4trl6fa7g', + 'name': 'Keller', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ggimpv4dqzkfs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ggimpv4dqzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Valve Controller', + 'model_id': 'd4vpmigg', + 'name': 'Garden Valve Yard', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ggwxkj8bwn5y63flgcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ggwxkj8bwn5y63flgcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'T & H Sensor', + 'model_id': 'lf36y5nwb8jkxwgg', + 'name': 'Greenhouse', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[gi69tunb0esxcnefzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'gi69tunb0esxcnefzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'smart plug', + 'model_id': 'fencxse0bnut96ig', + 'name': 'Spa', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[gjnpc0eojd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'gjnpc0eojd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Lighting', + 'model_id': 'oe0cpnjg', + 'name': 'Front right Lighting trap', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[glsehgu8jd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'glsehgu8jd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'POWERASIA', + 'model_id': '8ugheslg', + 'name': 'POWERASIA R2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[gluaktf5gk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'gluaktf5gk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '1Gang Zigbee Switch', + 'model_id': '5ftkaulg', + 'name': 'bathroom light', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[gnqwzcph94wj2sl5nq] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'gnqwzcph94wj2sl5nq', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Mr. Pure', + 'model_id': '5ls2jw49hpczwqng', + 'name': 'Mr. Pure', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[gt1q9tldv1opojrtcp] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'gt1q9tldv1opojrtcp', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Garden Spike(FR)', + 'model_id': 'trjopo1vdlt9q1tg', + 'name': 'Terras', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[gvxxy4jitzltz5xhscm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'gvxxy4jitzltz5xhscm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Door Detector', + 'model_id': 'hx5ztlztij4yxxvg', + 'name': 'Steel cage door', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[hfqeljop3aihnm73zc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'hfqeljop3aihnm73zc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'SP111', + 'model_id': '37mnhia3pojleqfh', + 'name': 'Sapphire ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[hkm4px9ohzozxma3rip] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'hkm4px9ohzozxma3rip', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Motion Sensor', + 'model_id': '3amxzozho9xp4mkh', + 'name': 'rat trap hedge', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[hxbonj4yzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'hxbonj4yzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '【通用接入】1路插座', + 'model_id': 'y4jnobxh', + 'name': 'AuVeLiCo', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[hyda5jsihokacvaqjzm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'hyda5jsihokacvaqjzm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Sous Vide', + 'model_id': 'qavcakohisj5adyh', + 'name': 'Sous Vide', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[hz4pau766eavmxhqsc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'hz4pau766eavmxhqsc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': ' (unsupported)', + 'model_id': 'qhxmvae667uap4zh', + 'name': 'DryFix', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[iaagy4qigcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'iaagy4qigcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Temperature Sensor', + 'model_id': 'iq4ygaai', + 'name': 'Bassin', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[idztlaspsms815moqkynw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'idztlaspsms815moqkynw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart IR (unsupported)', + 'model_id': 'om518smspsaltzdi', + 'name': 'Smart IR Theater', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ifzgvpgoodrfw2aksc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ifzgvpgoodrfw2aksc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Emma Dehumidifier - eeese air care', + 'model_id': 'ka2wfrdoogpvgzfi', + 'name': 'Dehumidifer', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ijne16zv8vpqmubnjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ijne16zv8vpqmubnjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LSC smart GU10', + 'model_id': 'nbumqpv8vz61enji', + 'name': 'b2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ijzjlqwmv1blwe0gsf] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ijzjlqwmv1blwe0gsf', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Ceiling Fan With Light', + 'model_id': 'g0ewlb1vmwqljzji', + 'name': 'Ceiling Fan With Light', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ikbbdbnqsd70pc1glc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ikbbdbnqsd70pc1glc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart roller blinds', + 'model_id': 'g1cp07dsqnbdbbki', + 'name': 'Persiana do Quarto', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[iks13mcaiyie3rryjb2oc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'iks13mcaiyie3rryjb2oc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'AIR_DETECTOR ', + 'model_id': 'yrr3eiyiacm31ski', + 'name': 'AQI', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ilms5pwjzzsxuxmvsc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ilms5pwjzzsxuxmvsc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'the Smart Dry Plus™ Connect Dehumidifier (unsupported)', + 'model_id': 'vmxuxszzjwp5smli', + 'name': 'Dehumidifier ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[im3fum2zt73boagkjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'im3fum2zt73boagkjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': 'kgaob37tz2muf3mi', + 'name': 'Parker Ceiling Fan 1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ingdwog22gw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ingdwog22gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Mesh-Gateway', + 'model_id': '2gowdgni', + 'name': 'Mesh-Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[iomszlsve0yyzkfwqswwc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'iomszlsve0yyzkfwqswwc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Cleverio PF100', + 'model_id': 'wfkzyy0evslzsmoi', + 'name': 'Cleverio PF100', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[j6mn1t4ut5end6ifkw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'j6mn1t4ut5end6ifkw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WiFi Smart Gas Boiler Thermostat ', + 'model_id': 'fi6dne5tu4t1nm6j', + 'name': 'WiFi Smart Gas Boiler Thermostat ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[jfpdpavoqgoqsn3cjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'jfpdpavoqgoqsn3cjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'RGBstriplight', + 'model_id': 'c3nsqogqovapdpfj', + 'name': 'Arbeitszimmer led', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[jfydgffzmhjed9fgjbwy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'jfydgffzmhjed9fgjbwy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Smoke Alarm', + 'model_id': 'gf9dejhmzffgdyfj', + 'name': ' Smoke detector upstairs ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[jlduh7vigcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'jlduh7vigcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Bluetooth Temperature Humidity Sensor', + 'model_id': 'iv7hudlj', + 'name': 'Basement temperature', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[jm2fsqtzuhqtbo5ykw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'jm2fsqtzuhqtbo5ykw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart', + 'model_id': 'y5obtqhuztqsf2mj', + 'name': 'Term - Prizemi', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kcdngswaxs8hm52bnocfw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kcdngswaxs8hm52bnocfw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ZigBee Gateway (unsupported)', + 'model_id': 'b25mh8sxawsgndck', + 'name': 'ZigBee Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kffnst1epj6vr8xnzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kffnst1epj6vr8xnzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart plug', + 'model_id': 'nx8rv6jpe1tsnffk', + 'name': 'Spot 1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kjr0pqg7eunn4vlujbgs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kjr0pqg7eunn4vlujbgs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Siren Sensor', + 'model_id': 'ulv4nnue7gqp0rjk', + 'name': 'Siren veranda ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kkande5hk6sfdkoxjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kkande5hk6sfdkoxjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LSC-G125-Gold ', + 'model_id': 'xokdfs6kh5ednakk', + 'name': 'ERKER 1-Gold ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[klgxmpwvdhw7tzs8jd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'klgxmpwvdhw7tzs8jd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Light Bulb', + 'model_id': '8szt7whdvwpmxglk', + 'name': 'Porch light E', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ksy8guiy64acbbpnqkynw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ksy8guiy64acbbpnqkynw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart IR (unsupported)', + 'model_id': 'npbbca46yiug8ysk', + 'name': 'Bedroom IR', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kta28zbwj6u0xa6lbsgy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kta28zbwj6u0xa6lbsgy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '水泵 (unsupported)', + 'model_id': 'l6ax0u6jwbz82atk', + 'name': 'Pond', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kvnsoqyfltmf0bknzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kvnsoqyfltmf0bknzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Konyks Pluviose Easy EU', + 'model_id': 'nkb0fmtlfyqosnvk', + 'name': 'Bassin', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kx8dncf1qzkfs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kx8dncf1qzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Valve Controller', + 'model_id': '1fcnd8xk', + 'name': 'Valve Controller 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kxwleaa2sph] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kxwleaa2sph', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Human presence sensor', + 'model_id': '2aaelwxk', + 'name': 'Human presence Office', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[kxxrbv93k2vvkconqdt] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'kxxrbv93k2vvkconqdt', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '6 Switch Smart RetroFit Module', + 'model_id': 'nockvv2k39vbrxxk', + 'name': 'Seating side 6-ch Smart Switch ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[l8uxezzkc7c5a0jhzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'l8uxezzkc7c5a0jhzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart plug', + 'model_id': 'hj0a5c7ckzzexu8l', + 'name': 'droger', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[lflvu8cazha8af9jsk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'lflvu8cazha8af9jsk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Tower Fan CA-407G Smart', + 'model_id': 'j9fa8ahzac8uvlfl', + 'name': 'Tower Fan CA-407G Smart', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[mgcpxpmovasazerdps] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'mgcpxpmovasazerdps', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Indoor camera ', + 'model_id': 'drezasavompxpcgm', + 'name': 'CAM GARAGE', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[mpowx36sgqexmtes2gw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'mpowx36sgqexmtes2gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': 'setmxeqgs63xwopm', + 'name': 'Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[mvsdcwtskkezlnw5tk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'mvsdcwtskkezlnw5tk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '移动空调 YPK--(双模+蓝牙)低功耗', + 'model_id': '5wnlzekkstwcdsvm', + 'name': 'Air Conditioner', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[mwsaod7fa3gjyh6ids] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'mwsaod7fa3gjyh6ids', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'E20', + 'model_id': 'i6hyjg3af7doaswm', + 'name': 'Hoover', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ncl7oi5d6hqmf1g0zc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ncl7oi5d6hqmf1g0zc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Mini Smart Plug', + 'model_id': '0g1fmqh6d5io7lcn', + 'name': 'Apollo light', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ngcubvaqoraolsmtjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ngcubvaqoraolsmtjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'G95-Filament', + 'model_id': 'tmsloaroqavbucgn', + 'name': 'Pokerlamp 1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[nnqlg0rxryraf8ezbdnz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'nnqlg0rxryraf8ezbdnz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'PJ2101A 1P WiFi Smart Meter ', + 'model_id': 'ze8faryrxr0glqnn', + 'name': 'Meter', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[nr26obpclc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'nr26obpclc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'curtain robot', + 'model_id': 'cpbo62rn', + 'name': 'blinds', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[nt3mpibadxfqkegldyg] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'nt3mpibadxfqkegldyg', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Colorful PIR Night Light', + 'model_id': 'lgekqfxdabipm3tn', + 'name': 'Colorful PIR Night Light', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[nxdcy0uidplnhkazjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'nxdcy0uidplnhkazjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LED SMART', + 'model_id': 'zakhnlpdiu0ycdxn', + 'name': 'Stoel', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[o71einxvuuktuljcjbwy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'o71einxvuuktuljcjbwy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smoke Alarm', + 'model_id': 'cjlutkuuvxnie17o', + 'name': 'Rauchmelder Alexsandro ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[obb7p55c0us6rdxkqld] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'obb7p55c0us6rdxkqld', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Metering_3PN_WiFi', + 'model_id': 'kxdr6su0c55p7bbo', + 'name': 'Metering_3PN_WiFi_stable', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ohefbbk9gcdl] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ohefbbk9gcdl', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Luminance sensor', + 'model_id': '9kbbfeho', + 'name': 'Luminosité', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[okwwus27jhqqe2mijbgs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'okwwus27jhqqe2mijbgs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Outdoor siren', + 'model_id': 'im2eqqhj72suwwko', + 'name': 'Siren', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ol8xwtcj42eg18bdbrnz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ol8xwtcj42eg18bdbrnz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Heat Pump', + 'model_id': 'db81ge24jctwx8lo', + 'name': 'Hot Water Heat Pump', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[oqyhsaqwsph] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'oqyhsaqwsph', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Soil moisture sensor', + 'model_id': 'wqashyqo', + 'name': 'Soil moisture sensor #1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[orotles4ucq8rxwn2gw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'orotles4ucq8rxwn2gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'X5', + 'model_id': 'nwxr8qcu4seltoro', + 'name': 'X5 Zigbee Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[owozxdzgbibizu4sjk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'owozxdzgbibizu4sjk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': 's4uzibibgzdxzowo', + 'name': 'ION1000PRO', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[p2gnclbiqxrbboagdd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'p2gnclbiqxrbboagdd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'TV Sync Backlights (unsupported)', + 'model_id': 'gaobbrxqiblcng2p', + 'name': 'TV Sync Backlights', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[p8xoxccrjbwy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'p8xoxccrjbwy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smoke Alarm', + 'model_id': 'rccxox8p', + 'name': 'Smoke Alarm', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[pdasfna8fswh4a0tzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'pdasfna8fswh4a0tzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug ', + 'model_id': 't0a4hwsf8anfsadp', + 'name': 'wallwasher front', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[pfhwb1v3i7cifa2tcp] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'pfhwb1v3i7cifa2tcp', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Garden Spike(EU)', + 'model_id': 't2afic7i3v1bwhfp', + 'name': 'Bubbelbad', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ppgdpsq1xaxlyzryjk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ppgdpsq1xaxlyzryjk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '40" Bladeless Tower Fan', + 'model_id': 'yrzylxax1qspdgpp', + 'name': 'Bree', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[pykascx9yfqrxtbgzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'pykascx9yfqrxtbgzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug+', + 'model_id': 'gbtxrqfy9xcsakyp', + 'name': '3DPrinter', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[pz2xuth8hczv6zrwzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'pz2xuth8hczv6zrwzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WiFi Plug', + 'model_id': 'wrz6vzch8htux2zp', + 'name': 'Elivco TV', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[q304vac40br8nlkajsywc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'q304vac40br8nlkajsywc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Pet Water Fountain', + 'model_id': 'akln8rb04cav403q', + 'name': 'Water Fountain', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[q3iie9vjd4wfqyy1qzkmkc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'q3iie9vjd4wfqyy1qzkmkc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Garage door ', + 'model_id': '1yyqfw4djv9eii3q', + 'name': 'Garage door ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[q62sg0p3s52thp6zzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'q62sg0p3s52thp6zzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '6294HA', + 'model_id': 'z6pht25s3p0gs26q', + 'name': '6294HA', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[q8dncqpgin4yympisc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'q8dncqpgin4yympisc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '30L Dehumidifier with Max Extraction', + 'model_id': 'ipmyy4nigpqcnd8q', + 'name': 'Pro Breeze 30L Compressor Dehumidifier', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[qhgghufzqtwloqoqjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'qhgghufzqtwloqoqjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Bulb RGBCW', + 'model_id': 'qoqolwtqzfuhgghq', + 'name': 'Smart Bulb RGBCW', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[qi94v9dmdx4fkpncqld] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'qi94v9dmdx4fkpncqld', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Breaker', + 'model_id': 'cnpkf4xdmd9v49iq', + 'name': '断路器HA', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[qifhbafbqubbp3b6qbnnz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'qifhbafbqubbp3b6qbnnz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Wi-Fi solar grid micro inverter (GT)', + 'model_id': '6b3pbbuqbfabhfiq', + 'name': 'Wi-Fi solar grid micro inverter (GT)', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[queafegmhhmtivdxjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'queafegmhhmtivdxjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'GU10 Smart Bulb', + 'model_id': 'xdvitmhhmgefaeuq', + 'name': 'druckerhell', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[qwExlkou9h2USezrjs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'qwExlkou9h2USezrjs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WATER SENSOR', + 'model_id': 'rzeSU2h9uoklxEwq', + 'name': 'Inondation', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[qyy1auihjyoogvb7zdccq] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'qyy1auihjyoogvb7zdccq', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'AC charging control box', + 'model_id': '7bvgooyjhiua1yyq', + 'name': 'AC charging control box', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[r4yrlr705ei31ikmjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'r4yrlr705ei31ikmjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Light Bulb', + 'model_id': 'mki13ie507rlry4r', + 'name': 'Garage light', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[rdq0bn4dzuwx2qfujd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'rdq0bn4dzuwx2qfujd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Ceiling Lamp', + 'model_id': 'ufq2xwuzd4nb0qdr', + 'name': 'Sjiethoes', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ri7eegdifufzdi54dyzb] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ri7eegdifufzdi54dyzb', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart White Noise Machine (unsupported)', + 'model_id': '45idzfufidgee7ir', + 'name': 'Smart White Noise Machine', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[rl39uwgaqwjwc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'rl39uwgaqwjwc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Odor Eliminator-Pro', + 'model_id': 'agwu93lr', + 'name': 'Smart Odor Eliminator-Pro', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[rojky4l6yyjreeilnocfw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'rojky4l6yyjreeilnocfw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Zigbee Smart Gateway (unsupported)', + 'model_id': 'lieerjyy6l4ykjor', + 'name': 'Zigbee Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[rsjdwgnbqky] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'rsjdwgnbqky', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Remote controller (unsupported)', + 'model_id': 'bngwdjsr', + 'name': 'Télécommande lumières ZigBee', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[rwp6kdezm97s2nktzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'rwp6kdezm97s2nktzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Socket ', + 'model_id': 'tkn2s79mzedk6pwr', + 'name': 'Weihnachtsmann ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[s3zzjdcfrip] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 's3zzjdcfrip', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Motion sensor', + 'model_id': 'fcdjzz3s', + 'name': 'Motion sensor lidl zigbee', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[sb3zdertrw50bgogkw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'sb3zdertrw50bgogkw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'smart thermostats', + 'model_id': 'gogb05wrtredz3bs', + 'name': 'smart thermostats', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[sdq2flqkq0lblcah2gw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'sdq2flqkq0lblcah2gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Multi-mode Gateway', + 'model_id': 'haclbl0qkqlf2qds', + 'name': 'Home Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[sifg4pfqsylsayg0jd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'sifg4pfqsylsayg0jd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': '0gyaslysqfp4gfis', + 'name': 'Study 1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[sj55nxhjftilowkejd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'sj55nxhjftilowkejd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LSC Smart Connect GU10 RGB+CCT', + 'model_id': 'ekwolitfjhxn55js', + 'name': 'ab6', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[snbu4b3vekhywztwqgcwy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'snbu4b3vekhywztwqgcwy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Tank A Level', + 'model_id': 'wtzwyhkev3b4ubns', + 'name': 'House Water Level', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[srp7cfjtn6sshwmt2gw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'srp7cfjtn6sshwmt2gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Gateway (unsupported)', + 'model_id': 'tmwhss6ntjfc7prs', + 'name': 'Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[svjjuwykgijjedurps] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'svjjuwykgijjedurps', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LSC PTZ Camera', + 'model_id': 'rudejjigkywujjvs', + 'name': 'Bürocam', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[sw1ejdomlmfubapizc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'sw1ejdomlmfubapizc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': 'ipabufmlmodje1ws', + 'name': 'Värmelampa', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[swhtzki3qrz5ydchjboc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'swhtzki3qrz5ydchjboc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WIFI smart CO alarm', + 'model_id': 'hcdy5zrq3ikzthws', + 'name': 'Smogo', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[t5zosev6h6wmwyrajbwy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 't5zosev6h6wmwyrajbwy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smoke Alarm ', + 'model_id': 'arywmw6h6vesoz5t', + 'name': 'Rauchmelder Drucker', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[t7bvnnvplkwhdqm9qtn] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 't7bvnnvplkwhdqm9qtn', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'TION Breezer Bio X (unsupported)', + 'model_id': '9mqdhwklpvnnvb7t', + 'name': 'Бризер Зал', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[tcdk0skzcpisexj2zc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'tcdk0skzcpisexj2zc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Dual channel metering', + 'model_id': '2jxesipczks0kdct', + 'name': 'HVAC Meter', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[thdfxdqqlc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'thdfxdqqlc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Blinds Drive-BLE', + 'model_id': 'qqdxfdht', + 'name': 'bedroom blinds', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[trffx1ktlyu3tnmljd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'trffx1ktlyu3tnmljd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LED SMART', + 'model_id': 'lmnt3uyltk1xffrt', + 'name': 'DirectietKamer', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[tskafaotnfigad6oqzkfs] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'tskafaotnfigad6oqzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Valve Controller', + 'model_id': 'o6dagifntoafakst', + 'name': 'Sprinkler Cesare', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[tvgoe1s3fabebcskjbwy] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'tvgoe1s3fabebcskjbwy', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WIFI Smoke alarm', + 'model_id': 'kscbebaf3s1eogvt', + 'name': 'WIFI Smoke alarm', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[uBLyTOvlhoRWXKjrps] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'uBLyTOvlhoRWXKjrps', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Indoor cam Pan/Tilt ', + 'model_id': 'rjKXWRohlvOTyLBu', + 'name': 'CAM PORCH', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[uew54dymycjwz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'uew54dymycjwz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Soil sensor', + 'model_id': 'myd45weu', + 'name': 'Patates', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[urm7i0rtdlabqiqygcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'urm7i0rtdlabqiqygcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WiFi Temperature & Humidity Sensor', + 'model_id': 'yqiqbaldtr0i7mru', + 'name': 'WiFi Temperature & Humidity Sensor', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[uvh6oeqrfliovfiwzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'uvh6oeqrfliovfiwzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Socket', + 'model_id': 'wifvoilfrqeo6hvu', + 'name': 'Licht drucker', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[vayhq2aj3p3z6y2ggcdsw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'vayhq2aj3p3z6y2ggcdsw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '温湿度传感器wifi', + 'model_id': 'g2y6z3p3ja2qhyav', + 'name': 'NP DownStairs North', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[vcrfgwvbuybgnj3zqld] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'vcrfgwvbuybgnj3zqld', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Breaker', + 'model_id': 'z3jngbyubvwgfrcv', + 'name': 'Edesanya Energy', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[vnj3sa6mqahro6phjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'vnj3sa6mqahro6phjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LED Strip Lights', + 'model_id': 'hp6orhaqm6as3jnv', + 'name': 'Master bedroom TV lights', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[vrhdtr5fawoiyth9qdt] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'vrhdtr5fawoiyth9qdt', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '1-433', + 'model_id': '9htyiowaf5rtdhrv', + 'name': 'Framboisiers', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[vx2owjsg86g2ys93zc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'vx2owjsg86g2ys93zc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Ineox SP2', + 'model_id': '39sy2g68gsjwo2xv', + 'name': 'Ineox SP2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[vzu7lkknqjz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'vzu7lkknqjz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Zigbee Repeater (unsupported)', + 'model_id': 'nkkl7uzv', + 'name': 'Zigby répéteur ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[w8oht6v8aauqa0y8jd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'w8oht6v8aauqa0y8jd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'A60 Clear', + 'model_id': '8y0aquaa8v6tho8w', + 'name': 'dressoir spot', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[wc6mumew8inrivi9zc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'wc6mumew8inrivi9zc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Socket', + 'model_id': '9ivirni8wemum6cw', + 'name': 'Garáž čerpadlo', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[weozorgv28n2scribswh] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'weozorgv28n2scribswh', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'InverFlow (unsupported)', + 'model_id': 'ircs2n82vgrozoew', + 'name': 'InverFlow', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[x4nogasbi8ggpb3lcd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'x4nogasbi8ggpb3lcd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LSC Party String Light RGBIC+CCT ', + 'model_id': 'l3bpgg8ibsagon4x', + 'name': 'LSC Party String Light RGBIC+CCT ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[x7quooqakw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'x7quooqakw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'T7-Air conditioner thermostat(ZIGBEE)', + 'model_id': 'aqoouq7x', + 'name': 'Clima cucina', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[xenxir4a0tn0p1qcqdt] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'xenxir4a0tn0p1qcqdt', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '4-433', + 'model_id': 'cq1p0nt0a4rixnex', + 'name': '4-433', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[yky6kunazmaitupzjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'yky6kunazmaitupzjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LSC Floodlight', + 'model_id': 'zputiamzanuk6yky', + 'name': 'Floodlight', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[yo2karkjuhzztxsfjk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'yo2karkjuhzztxsfjk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': 'fsxtzzhujkrak2oy', + 'name': 'Kalado Air Purifier', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[yuanswy6scm] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'yuanswy6scm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Contact Sensor', + 'model_id': '6ywsnauy', + 'name': 'Fenêtre cuisine', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[z7cu5t8bl9tt9fabjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'z7cu5t8bl9tt9fabjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'LED SMART', + 'model_id': 'baf9tt9lb8t5uc7z', + 'name': 'Pokerlamp 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[z8woiryqydmzonjdjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'z8woiryqydmzonjdjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Candle RGB-CCT', + 'model_id': 'djnozmdyqyriow8z', + 'name': 'Fakkel 8', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zaszonjgzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zaszonjgzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart plug', + 'model_id': 'gjnozsaz', + 'name': 'Raspy4 - Home Assistant', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zf8vgiwoa07jwegtjd] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zf8vgiwoa07jwegtjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'RGBC Smart Bulb', + 'model_id': 'tgewj70aowigv8fz', + 'name': 'Stairs', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zfHZQ7tZUBxAWjACjk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zfHZQ7tZUBxAWjACjk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'air purifier', + 'model_id': 'CAjWAxBUZt7QZHfz', + 'name': 'HL400', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zgiyrxflahjowpcckw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zgiyrxflahjowpcckw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Intelligent temperature controller', + 'model_id': 'ccpwojhalfxryigz', + 'name': 'Boiler Temperature Controller', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zjh9xhtm3gibs9kizc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zjh9xhtm3gibs9kizc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Aubess Smart\xa0Socket EM', + 'model_id': 'ik9sbig3mthx9hjz', + 'name': 'Aubess Washing Machine', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zoytcemodrn39zqwrip] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zoytcemodrn39zqwrip', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart PIR sensor', + 'model_id': 'wqz93nrdomectyoz', + 'name': 'PIR outside stairs', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zrrraytdoanz33rlds] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zrrraytdoanz33rlds', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'V20', + 'model_id': 'lr33znaodtyarrrz', + 'name': 'V20', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zspc4q1ut7swycnyzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zspc4q1ut7swycnyzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Wi-Fi hub', + 'model_id': 'yncyws7tu1q4cpsz', + 'name': 'Wi-Fi hub', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zspxfhsvgn2hgtndzc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zspxfhsvgn2hgtndzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Plug', + 'model_id': 'dntgh2ngvshfxpsz', + 'name': 'fakkel veranda ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[zyutbek7wdm1b4cgzckw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zyutbek7wdm1b4cgzckw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '4-TH', + 'model_id': 'gc4b1mdw7kebtuyz', + 'name': 'pid_relay_2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) # --- diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 4f2f22ddf2b..c04cee4a46d 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -1,889 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': , - 'entity_id': 'light.tapparelle_studio_backlight', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Backlight', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'backlight', - 'unique_id': 'tuya.bf1fa053e0ba4e002c6we8switch_backlight', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[clkg_nhyj64w2][light.tapparelle_studio_backlight-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'color_mode': , - 'friendly_name': 'Tapparelle studio Backlight', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.tapparelle_studio_backlight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[dc_l3bpgg8ibsagon4x][light.lsc_party_string_light_rgbic_cct-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.lsc_party_string_light_rgbic_cct', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bfd9f45c6b882c9f46dxfcswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dc_l3bpgg8ibsagon4x][light.lsc_party_string_light_rgbic_cct-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LSC Party String Light RGBIC+CCT ', - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.lsc_party_string_light_rgbic_cct', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dj_8szt7whdvwpmxglk][light.porch_light_e-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.porch_light_e', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.eb10549aadfc74b7c8q2tiswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_8szt7whdvwpmxglk][light.porch_light_e-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Porch light E', - 'hs_color': None, - 'rgb_color': None, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.porch_light_e', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_8y0aquaa8v6tho8w][light.dressoir_spot-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.dressoir_spot', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf71858c3d27943679dsx9switch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_8y0aquaa8v6tho8w][light.dressoir_spot-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'dressoir spot', - 'hs_color': None, - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.dressoir_spot', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_baf9tt9lb8t5uc7z][light.pokerlamp_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.pokerlamp_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.40611462e09806c73134switch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_baf9tt9lb8t5uc7z][light.pokerlamp_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Pokerlamp 2', - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.pokerlamp_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dj_d4g0fbsoaal841o6][light.wc_d1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.wc_d1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf671413db4cee1f9bqdcxswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_d4g0fbsoaal841o6][light.wc_d1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'WC D1', - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.wc_d1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dj_djnozmdyqyriow8z][light.fakkel_8-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.fakkel_8', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf8885f3d18a73e395bfacswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_djnozmdyqyriow8z][light.fakkel_8-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 70, - 'color_mode': , - 'color_temp': 500, - 'color_temp_kelvin': 2000, - 'friendly_name': 'Fakkel 8', - 'hs_color': tuple( - 30.601, - 94.547, - ), - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 137, - 14, - ), - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.598, - 0.383, - ), - }), - 'context': , - 'entity_id': 'light.fakkel_8', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[dj_ekwolitfjhxn55js][light.ab6-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.ab6', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bfb99bba00c9c90ba8gzglswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_ekwolitfjhxn55js][light.ab6-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'ab6', - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.ab6', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dj_fuupmcr2mb1odkja][light.slaapkamer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.slaapkamer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf0914a82b06ecf151xsf5switch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_fuupmcr2mb1odkja][light.slaapkamer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Slaapkamer', - 'hs_color': None, - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.slaapkamer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_hp6orhaqm6as3jnv][light.master_bedroom_tv_lights-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.master_bedroom_tv_lights', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.00450321483fda81c529switch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_hp6orhaqm6as3jnv][light.master_bedroom_tv_lights-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 51, - 'color_mode': , - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Master bedroom TV lights', - 'hs_color': tuple( - 26.072, - 100.0, - ), - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 111, - 0, - ), - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.632, - 0.358, - ), - }), - 'context': , - 'entity_id': 'light.master_bedroom_tv_lights', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.garage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.63362034840d8eb9029fswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Garage', - 'hs_color': None, - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.garage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage_light_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.garage_light_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Light 1', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_light', - 'unique_id': 'tuya.63362034840d8eb9029fswitch_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_hpc8ddyfv85haxa7][light.garage_light_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'color_mode': None, - 'friendly_name': 'Garage Light 1', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.garage_light_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_iayz2jmtlipjnxj7][light.led_porch_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.led_porch_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf0fc1d7d4caa71a59us7cswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_iayz2jmtlipjnxj7][light.led_porch_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'LED Porch 2', - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.led_porch_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dj_idnfq7xbx8qewyoa][light.ab1-entry] +# name: test_platform_setup_and_discovery[light.ab1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -923,11 +39,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf599f5cffe1a5985depykswitch_led', + 'unique_id': 'tuya.aoyweq8xbx7qfndijdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_idnfq7xbx8qewyoa][light.ab1-state] +# name: test_platform_setup_and_discovery[light.ab1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': 255, @@ -966,7 +82,1610 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dj_ilddqqih3tucdk68][light.ieskas-entry] +# name: test_platform_setup_and_discovery[light.ab6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ab6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.sj55nxhjftilowkejdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.ab6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ab6', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.ab6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.arbeitszimmer_led-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.arbeitszimmer_led', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.jfpdpavoqgoqsn3cjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.arbeitszimmer_led-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'Arbeitszimmer led', + 'hs_color': tuple( + 0.0, + 100.0, + ), + 'rgb_color': tuple( + 255, + 0, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.701, + 0.299, + ), + }), + 'context': , + 'entity_id': 'light.arbeitszimmer_led', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.b2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.b2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.ijne16zv8vpqmubnjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.b2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'b2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.b2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.bedroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.bedroom', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.CyD4ctKVrAFSSXSbjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.bedroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'bedroom', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.bedroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.cam_garage_indicator_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.cam_garage_indicator_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.mgcpxpmovasazerdpsbasic_indicator', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.cam_garage_indicator_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'CAM GARAGE Indicator light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.cam_garage_indicator_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.cam_porch_indicator_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.cam_porch_indicator_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsbasic_indicator', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.cam_porch_indicator_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'CAM PORCH Indicator light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.cam_porch_indicator_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.ceiling_fan_light_v2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ceiling_fan_light_v2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.6wxksqu35c61sce9dsfswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.ceiling_fan_light_v2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'ceiling fan/Light v2', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.ceiling_fan_light_v2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.ceiling_fan_with_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ceiling_fan_with_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.ijzjlqwmv1blwe0gsflight', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.ceiling_fan_with_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'Ceiling Fan With Light', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.ceiling_fan_with_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.ceiling_portal-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ceiling_portal', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.buzituffc13pgb1jjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.ceiling_portal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Ceiling Portal', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.ceiling_portal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.colorful_pir_night_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.colorful_pir_night_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.nt3mpibadxfqkegldygswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.colorful_pir_night_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Colorful PIR Night Light', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.colorful_pir_night_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.directietkamer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.directietkamer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.trffx1ktlyu3tnmljdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.directietkamer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'DirectietKamer', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.directietkamer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.dressoir_spot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dressoir_spot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.w8oht6v8aauqa0y8jdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.dressoir_spot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'dressoir spot', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.dressoir_spot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.druckerhell-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.druckerhell', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.queafegmhhmtivdxjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.druckerhell-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'druckerhell', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.druckerhell', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.entry_stairs-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.entry_stairs', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.VA4QyBNZHkJ2Xa4hjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.entry_stairs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Entry Stairs', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.entry_stairs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.erker_1_gold-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.erker_1_gold', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.kkande5hk6sfdkoxjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.erker_1_gold-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'ERKER 1-Gold ', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.erker_1_gold', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.fakkel_8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.fakkel_8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.z8woiryqydmzonjdjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.fakkel_8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 70, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'Fakkel 8', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.fakkel_8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.floodlight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.floodlight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.yky6kunazmaitupzjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.floodlight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Floodlight', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.floodlight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.front_right_lighting_trap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.front_right_lighting_trap', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.gjnpc0eojdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.front_right_lighting_trap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Front right Lighting trap', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.front_right_lighting_trap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.garage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.garage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.7axah58vfydd8cphjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.garage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Garage', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.garage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.garage_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.garage_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.r4yrlr705ei31ikmjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.garage_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 138, + 'color_mode': , + 'friendly_name': 'Garage light', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.garage_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.garage_light_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.garage_light_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_light', + 'unique_id': 'tuya.7axah58vfydd8cphjdswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.garage_light_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Garage Light 1', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.garage_light_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.gengske-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.gengske', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.87yarxyp23ap1vazjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.gengske-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Gengske 💡 ', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.gengske', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.hall-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hall', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.6gsqieoh1yzjvxlnjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.hall-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'hall 💡 ', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.hall', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.ieskas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1005,11 +1724,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.84178216d8f15be52dc4switch_led', + 'unique_id': 'tuya.86kdcut3hiqqddlijdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_ilddqqih3tucdk68][light.ieskas-state] +# name: test_platform_setup_and_discovery[light.ieskas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': 255, @@ -1047,7 +1766,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dj_j1bgp31cffutizub][light.ceiling_portal-entry] +# name: test_platform_setup_and_discovery[light.landing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1069,7 +1788,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.ceiling_portal', + 'entity_id': 'light.landing', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1087,18 +1806,18 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bfe49d7b6cd80536efdldiswitch_led', + 'unique_id': 'tuya.4fO1qIzYbcdMUHqAjdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_j1bgp31cffutizub][light.ceiling_portal-state] +# name: test_platform_setup_and_discovery[light.landing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': None, 'color_mode': None, 'color_temp': None, 'color_temp_kelvin': None, - 'friendly_name': 'Ceiling Portal', + 'friendly_name': 'Landing', 'hs_color': None, 'max_color_temp_kelvin': 6500, 'max_mireds': 500, @@ -1113,358 +1832,14 @@ 'xy_color': None, }), 'context': , - 'entity_id': 'light.ceiling_portal', + 'entity_id': 'light.landing', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[dj_lmnt3uyltk1xffrt][light.directietkamer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.directietkamer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.07608286600194e94248switch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_lmnt3uyltk1xffrt][light.directietkamer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'DirectietKamer', - 'hs_color': None, - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.directietkamer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.garage_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.REDACTEDswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_mki13ie507rlry4r][light.garage_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 138, - 'color_mode': , - 'friendly_name': 'Garage light', - 'hs_color': None, - 'rgb_color': None, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.garage_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[dj_nbumqpv8vz61enji][light.b2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.b2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf77c04cbd6a52a7be16llswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_nbumqpv8vz61enji][light.b2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'b2', - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.b2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dj_nlxvjzy1hoeiqsg6][light.hall-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.hall', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.40350105dc4f229a464eswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_nlxvjzy1hoeiqsg6][light.hall-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'hall 💡 ', - 'hs_color': None, - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.hall', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_oe0cpnjg][light.front_right_lighting_trap-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.front_right_lighting_trap', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf8d8af3ddfe75b0195r0hswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_oe0cpnjg][light.front_right_lighting_trap-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Front right Lighting trap', - 'hs_color': None, - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.front_right_lighting_trap', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[dj_riwp3k79][light.led_keuken_2-entry] +# name: test_platform_setup_and_discovery[light.led_keuken_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1504,11 +1879,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf46b2b81ca41ce0c1xpswswitch_led', + 'unique_id': 'tuya.97k3pwirjdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_riwp3k79][light.led_keuken_2-state] +# name: test_platform_setup_and_discovery[light.led_keuken_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': 255, @@ -1547,7 +1922,432 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[dj_tmsloaroqavbucgn][light.pokerlamp_1-entry] +# name: test_platform_setup_and_discovery[light.led_porch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.led_porch_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.7jxnjpiltmj2zyaijdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.led_porch_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LED Porch 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.led_porch_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.lsc_party_string_light_rgbic_cct-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.lsc_party_string_light_rgbic_cct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.x4nogasbi8ggpb3lcdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.lsc_party_string_light_rgbic_cct-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LSC Party String Light RGBIC+CCT ', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.lsc_party_string_light_rgbic_cct', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.lumy_garage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.lumy_garage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.4pa1uobdjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.lumy_garage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Lumy Garage', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.lumy_garage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.lumy_hall-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.lumy_hall', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.bgnj6bafrdgb1xmajdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.lumy_hall-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Lumy Hall', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.lumy_hall', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.master_bedroom_tv_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.master_bedroom_tv_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.vnj3sa6mqahro6phjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.master_bedroom_tv_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 51, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Master bedroom TV lights', + 'hs_color': tuple( + 26.072, + 100.0, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 111, + 0, + ), + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.632, + 0.358, + ), + }), + 'context': , + 'entity_id': 'light.master_bedroom_tv_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.parker_ceiling_fan_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.parker_ceiling_fan_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.im3fum2zt73boagkjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.parker_ceiling_fan_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Parker Ceiling Fan 1', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.parker_ceiling_fan_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.pokerlamp_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1586,11 +2386,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf252b8ee16b2e78bdoxlpswitch_led', + 'unique_id': 'tuya.ngcubvaqoraolsmtjdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_tmsloaroqavbucgn][light.pokerlamp_1-state] +# name: test_platform_setup_and_discovery[light.pokerlamp_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Pokerlamp 1', @@ -1611,7 +2411,205 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[dj_ufq2xwuzd4nb0qdr][light.sjiethoes-entry] +# name: test_platform_setup_and_discovery[light.pokerlamp_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.pokerlamp_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.z7cu5t8bl9tt9fabjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.pokerlamp_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pokerlamp 2', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.pokerlamp_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.porch_light_e-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.porch_light_e', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.klgxmpwvdhw7tzs8jdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.porch_light_e-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Porch light E', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.porch_light_e', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.powerasia_r2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.powerasia_r2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.glsehgu8jdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.powerasia_r2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'POWERASIA R2', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.powerasia_r2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[light.sjiethoes-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1650,11 +2648,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bf8edbd51a52c01a4bfgqfswitch_led', + 'unique_id': 'tuya.rdq0bn4dzuwx2qfujdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_ufq2xwuzd4nb0qdr][light.sjiethoes-state] +# name: test_platform_setup_and_discovery[light.sjiethoes-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Sjiethoes', @@ -1675,7 +2673,334 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[dj_vqwcnabamzrc2kab][light.strip_2-entry] +# name: test_platform_setup_and_discovery[light.slaapkamer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.slaapkamer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.ajkdo1bm2rcmpuufjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.slaapkamer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Slaapkamer', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.slaapkamer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.smart_bulb_rgbcw-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.smart_bulb_rgbcw', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.qhgghufzqtwloqoqjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.smart_bulb_rgbcw-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'Smart Bulb RGBCW', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.smart_bulb_rgbcw', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.solar_zijpad-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.solar_zijpad', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.couukaypjdnytswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.solar_zijpad-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Solar zijpad', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.solar_zijpad', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.stairs-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.stairs', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.zf8vgiwoa07jwegtjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.stairs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Stairs', + 'hs_color': None, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.stairs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[light.stoel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.stoel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.nxdcy0uidplnhkazjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.stoel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Stoel', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.stoel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[light.strip_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1715,11 +3040,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bfd56f4718874ee8830xdwswitch_led', + 'unique_id': 'tuya.bak2crzmabancwqvjdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_vqwcnabamzrc2kab][light.strip_2-state] +# name: test_platform_setup_and_discovery[light.strip_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Strip 2', @@ -1741,152 +3066,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[dj_xokdfs6kh5ednakk][light.erker_1_gold-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.erker_1_gold', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bfc1ef4da4accc0731oggwswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_xokdfs6kh5ednakk][light.erker_1_gold-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'color_temp': 500, - 'color_temp_kelvin': 2000, - 'friendly_name': 'ERKER 1-Gold ', - 'hs_color': tuple( - 30.601, - 94.547, - ), - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 137, - 14, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.598, - 0.383, - ), - }), - 'context': , - 'entity_id': 'light.erker_1_gold', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[dj_zakhnlpdiu0ycdxn][light.stoel-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.stoel', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.03010850c44f33966362switch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dj_zakhnlpdiu0ycdxn][light.stoel-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Stoel', - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.stoel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dj_zav1pa32pyxray78][light.gengske-entry] +# name: test_platform_setup_and_discovery[light.study_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1908,7 +3088,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.gengske', + 'entity_id': 'light.study_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1926,18 +3106,18 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.500425642462ab50909bswitch_led', + 'unique_id': 'tuya.sifg4pfqsylsayg0jdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_zav1pa32pyxray78][light.gengske-state] +# name: test_platform_setup_and_discovery[light.study_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': None, 'color_mode': None, 'color_temp': None, 'color_temp_kelvin': None, - 'friendly_name': 'Gengske 💡 ', + 'friendly_name': 'Study 1', 'hs_color': None, 'max_color_temp_kelvin': 6500, 'max_mireds': 500, @@ -1952,22 +3132,21 @@ 'xy_color': None, }), 'context': , - 'entity_id': 'light.gengske', + 'entity_id': 'light.study_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[dj_zputiamzanuk6yky][light.floodlight-entry] +# name: test_platform_setup_and_discovery[light.tapparelle_studio_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'supported_color_modes': list([ - , - , + , ]), }), 'config_entry_id': , @@ -1976,8 +3155,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.floodlight', + 'entity_category': , + 'entity_id': 'light.tapparelle_studio_backlight', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1989,194 +3168,35 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Backlight', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf74164049de868395pbciswitch_led', + 'translation_key': 'backlight', + 'unique_id': 'tuya.2w46jyhngklcswitch_backlight', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[dj_zputiamzanuk6yky][light.floodlight-state] +# name: test_platform_setup_and_discovery[light.tapparelle_studio_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'friendly_name': 'Floodlight', - 'hs_color': None, - 'rgb_color': None, + 'color_mode': , + 'friendly_name': 'Tapparelle studio Backlight', 'supported_color_modes': list([ - , - , + , ]), 'supported_features': , - 'xy_color': None, }), 'context': , - 'entity_id': 'light.floodlight', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][light.ceiling_fan_with_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.ceiling_fan_with_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.XXXlight', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][light.ceiling_fan_with_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': 255, - 'color_mode': , - 'color_temp': 500, - 'color_temp_kelvin': 2000, - 'friendly_name': 'Ceiling Fan With Light', - 'hs_color': tuple( - 30.601, - 94.547, - ), - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': tuple( - 255, - 137, - 14, - ), - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - 'xy_color': tuple( - 0.598, - 0.383, - ), - }), - 'context': , - 'entity_id': 'light.ceiling_fan_with_light', + 'entity_id': 'light.tapparelle_studio_backlight', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'supported_color_modes': list([ - , - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': None, - 'entity_id': 'light.colorful_pir_night_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.eb3e988f33c233290cfs3lswitch_led', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[gyd_lgekqfxdabipm3tn][light.colorful_pir_night_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'brightness': None, - 'color_mode': None, - 'color_temp': None, - 'color_temp_kelvin': None, - 'friendly_name': 'Colorful PIR Night Light', - 'hs_color': None, - 'max_color_temp_kelvin': 6500, - 'max_mireds': 500, - 'min_color_temp_kelvin': 2000, - 'min_mireds': 153, - 'rgb_color': None, - 'supported_color_modes': list([ - , - , - ]), - 'supported_features': , - 'xy_color': None, - }), - 'context': , - 'entity_id': 'light.colorful_pir_night_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-entry] +# name: test_platform_setup_and_discovery[light.tower_fan_ca_407g_smart_backlight-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2211,11 +3231,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backlight', - 'unique_id': 'tuya.mock_device_idlight', + 'unique_id': 'tuya.lflvu8cazha8af9jsklight', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][light.tower_fan_ca_407g_smart_backlight-state] +# name: test_platform_setup_and_discovery[light.tower_fan_ca_407g_smart_backlight-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': , @@ -2233,128 +3253,18 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][light.cam_garage_indicator_light-entry] +# name: test_platform_setup_and_discovery[light.wc_d1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': , - 'entity_id': 'light.cam_garage_indicator_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Indicator light', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_indicator', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][light.cam_garage_indicator_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'color_mode': , - 'friendly_name': 'CAM GARAGE Indicator light', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.cam_garage_indicator_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][light.cam_porch_indicator_light-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'light', - 'entity_category': , - 'entity_id': 'light.cam_porch_indicator_light', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Indicator light', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_indicator', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][light.cam_porch_indicator_light-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'color_mode': None, - 'friendly_name': 'CAM PORCH Indicator light', - 'supported_color_modes': list([ - , - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'light.cam_porch_indicator_light', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'supported_color_modes': list([ - , + , ]), }), 'config_entry_id': , @@ -2364,7 +3274,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.solar_zijpad', + 'entity_id': 'light.wc_d1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2382,21 +3292,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_led', + 'unique_id': 'tuya.6o148laaosbf0g4djdswitch_led', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][light.solar_zijpad-state] +# name: test_platform_setup_and_discovery[light.wc_d1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Solar zijpad', + 'friendly_name': 'WC D1', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, 'supported_color_modes': list([ - , + , ]), 'supported_features': , }), 'context': , - 'entity_id': 'light.solar_zijpad', + 'entity_id': 'light.wc_d1', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index b5d6224ecea..bc49b03cd36 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-entry] +# name: test_platform_setup_and_discovery[number.aqi_alarm_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -35,11 +35,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm_duration', - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinalarm_time', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2ocalarm_time', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][number.aqi_alarm_duration-state] +# name: test_platform_setup_and_discovery[number.aqi_alarm_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -58,7 +58,123 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-entry] +# name: test_platform_setup_and_discovery[number.boiler_temperature_controller_temperature_correction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 9.9, + 'min': -9.9, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.boiler_temperature_controller_temperature_correction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature correction', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temp_correction', + 'unique_id': 'tuya.zgiyrxflahjowpcckwtemp_correction', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[number.boiler_temperature_controller_temperature_correction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Boiler Temperature Controller Temperature correction', + 'max': 9.9, + 'min': -9.9, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.boiler_temperature_controller_temperature_correction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-0.8', + }) +# --- +# name: test_platform_setup_and_discovery[number.c9_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.c9_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsbasic_device_volume', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[number.c9_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'C9 Volume', + 'max': 10.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'number.c9_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.cleverio_pf100_feed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -94,11 +210,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'feed', - 'unique_id': 'tuya.bfd0273e59494eb34esvrxmanual_feed', + 'unique_id': 'tuya.iomszlsve0yyzkfwqswwcmanual_feed', 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][number.cleverio_pf100_feed-state] +# name: test_platform_setup_and_discovery[number.cleverio_pf100_feed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Cleverio PF100 Feed', @@ -116,7 +232,300 @@ 'state': '1.0', }) # --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_far_detection-entry] +# name: test_platform_setup_and_discovery[number.hot_water_heat_pump_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 75.0, + 'min': 15.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.hot_water_heat_pump_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.ol8xwtcj42eg18bdbrnztemp_set', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[number.hot_water_heat_pump_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Hot Water Heat Pump Temperature', + 'max': 75.0, + 'min': 15.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.hot_water_heat_pump_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_alarm_maximum-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.house_water_level_alarm_maximum', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm maximum', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_maximum', + 'unique_id': 'tuya.snbu4b3vekhywztwqgcwymax_set', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_alarm_maximum-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'House Water Level Alarm maximum', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.house_water_level_alarm_maximum', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_alarm_minimum-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.house_water_level_alarm_minimum', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm minimum', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_minimum', + 'unique_id': 'tuya.snbu4b3vekhywztwqgcwymini_set', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_alarm_minimum-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'House Water Level Alarm minimum', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.house_water_level_alarm_minimum', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_installation_height-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 2.5, + 'min': 0.2, + 'mode': , + 'step': 0.001, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.house_water_level_installation_height', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Installation height', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'installation_height', + 'unique_id': 'tuya.snbu4b3vekhywztwqgcwyinstallation_height', + 'unit_of_measurement': 'm', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_installation_height-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'House Water Level Installation height', + 'max': 2.5, + 'min': 0.2, + 'mode': , + 'step': 0.001, + 'unit_of_measurement': 'm', + }), + 'context': , + 'entity_id': 'number.house_water_level_installation_height', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.56', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_maximum_liquid_depth-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 2.4, + 'min': 0.1, + 'mode': , + 'step': 0.001, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.house_water_level_maximum_liquid_depth', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum liquid depth', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'maximum_liquid_depth', + 'unique_id': 'tuya.snbu4b3vekhywztwqgcwyliquid_depth_max', + 'unit_of_measurement': 'm', + }) +# --- +# name: test_platform_setup_and_discovery[number.house_water_level_maximum_liquid_depth-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'House Water Level Maximum liquid depth', + 'max': 2.4, + 'min': 0.1, + 'mode': , + 'step': 0.001, + 'unit_of_measurement': 'm', + }), + 'context': , + 'entity_id': 'number.house_water_level_maximum_liquid_depth', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) +# --- +# name: test_platform_setup_and_discovery[number.human_presence_office_far_detection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -152,11 +561,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'far_detection', - 'unique_id': 'tuya.bf78687ad321a3aeb8a73mfar_detection', + 'unique_id': 'tuya.kxwleaa2sphfar_detection', 'unit_of_measurement': 'cm', }) # --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_far_detection-state] +# name: test_platform_setup_and_discovery[number.human_presence_office_far_detection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'distance', @@ -175,7 +584,7 @@ 'state': '220.0', }) # --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_near_detection-entry] +# name: test_platform_setup_and_discovery[number.human_presence_office_near_detection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -211,11 +620,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'near_detection', - 'unique_id': 'tuya.bf78687ad321a3aeb8a73mnear_detection', + 'unique_id': 'tuya.kxwleaa2sphnear_detection', 'unit_of_measurement': 'cm', }) # --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_near_detection-state] +# name: test_platform_setup_and_discovery[number.human_presence_office_near_detection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'distance', @@ -234,7 +643,7 @@ 'state': '40.0', }) # --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_sensitivity-entry] +# name: test_platform_setup_and_discovery[number.human_presence_office_sensitivity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -270,11 +679,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensitivity', - 'unique_id': 'tuya.bf78687ad321a3aeb8a73msensitivity', + 'unique_id': 'tuya.kxwleaa2sphsensitivity', 'unit_of_measurement': 'x', }) # --- -# name: test_platform_setup_and_discovery[hps_2aaelwxk][number.human_presence_office_sensitivity-state] +# name: test_platform_setup_and_discovery[number.human_presence_office_sensitivity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Human presence Office Sensitivity', @@ -292,7 +701,537 @@ 'state': '3.0', }) # --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-entry] +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_1', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 1', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_2', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 2', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 3', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_3', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 3', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 4', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_4', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 4', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 5', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_5', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 5', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 6', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_6', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 6', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_7-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_7', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 7', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_7', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_7-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 7', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_7', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Irrigation duration 8', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_irrigation_duration', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfscountdown_8', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.jie_hashui_fa_irrigation_duration_8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '接HA水阀 Irrigation duration 8', + 'max': 1439.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.jie_hashui_fa_irrigation_duration_8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.kabinet_temperature_correction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 9.0, + 'min': -9.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.kabinet_temperature_correction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature correction', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temp_correction', + 'unique_id': 'tuya.dn7cjik6kwtemp_correction', + 'unit_of_measurement': '℃', + }) +# --- +# name: test_platform_setup_and_discovery[number.kabinet_temperature_correction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Кабінет Temperature correction', + 'max': 9.0, + 'min': -9.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '℃', + }), + 'context': , + 'entity_id': 'number.kabinet_temperature_correction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-2.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.multifunction_alarm_alarm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -328,11 +1267,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm_delay', - 'unique_id': 'tuya.123123aba12312312dazubalarm_delay_time', + 'unique_id': 'tuya.2pxfek1jjrtctiyglamalarm_delay_time', 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_alarm_delay-state] +# name: test_platform_setup_and_discovery[number.multifunction_alarm_alarm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -351,7 +1290,7 @@ 'state': '20.0', }) # --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-entry] +# name: test_platform_setup_and_discovery[number.multifunction_alarm_arm_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -387,11 +1326,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'arm_delay', - 'unique_id': 'tuya.123123aba12312312dazubdelay_set', + 'unique_id': 'tuya.2pxfek1jjrtctiyglamdelay_set', 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_arm_delay-state] +# name: test_platform_setup_and_discovery[number.multifunction_alarm_arm_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -410,7 +1349,7 @@ 'state': '15.0', }) # --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-entry] +# name: test_platform_setup_and_discovery[number.multifunction_alarm_siren_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -446,11 +1385,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'siren_duration', - 'unique_id': 'tuya.123123aba12312312dazubalarm_time', + 'unique_id': 'tuya.2pxfek1jjrtctiyglamalarm_time', 'unit_of_measurement': 'min', }) # --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][number.multifunction_alarm_siren_duration-state] +# name: test_platform_setup_and_discovery[number.multifunction_alarm_siren_duration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -469,123 +1408,7 @@ 'state': '3.0', }) # --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][number.sous_vide_cook_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 92.5, - 'min': 25.0, - 'mode': , - 'step': 0.1, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.sous_vide_cook_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cook temperature', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'cook_temperature', - 'unique_id': 'tuya.bff434eca843ffc9afmthvcook_temperature', - 'unit_of_measurement': '℃', - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][number.sous_vide_cook_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sous Vide Cook temperature', - 'max': 92.5, - 'min': 25.0, - 'mode': , - 'step': 0.1, - 'unit_of_measurement': '℃', - }), - 'context': , - 'entity_id': 'number.sous_vide_cook_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][number.sous_vide_cook_time-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 5999.0, - 'min': 1.0, - 'mode': , - 'step': 1.0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.sous_vide_cook_time', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cook time', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'cook_time', - 'unique_id': 'tuya.bff434eca843ffc9afmthvcook_time', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][number.sous_vide_cook_time-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sous Vide Cook time', - 'max': 5999.0, - 'min': 1.0, - 'mode': , - 'step': 1.0, - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'number.sous_vide_cook_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][number.v20_volume-entry] +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_alarm_maximum-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -603,7 +1426,7 @@ 'disabled_by': None, 'domain': 'number', 'entity_category': , - 'entity_id': 'number.v20_volume', + 'entity_id': 'number.rainwater_tank_level_alarm_maximum', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -615,20 +1438,20 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Volume', + 'original_name': 'Alarm maximum', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'volume', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtvolume_set', + 'translation_key': 'alarm_maximum', + 'unique_id': 'tuya.fbya6s6rhaoyvl8hqgcwymax_set', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][number.v20_volume-state] +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_alarm_maximum-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Volume', + 'friendly_name': 'Rainwater Tank Level Alarm maximum', 'max': 100.0, 'min': 0.0, 'mode': , @@ -636,14 +1459,248 @@ 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'number.v20_volume', + 'entity_id': 'number.rainwater_tank_level_alarm_maximum', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '95.0', + 'state': '90.0', }) # --- -# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][number.siren_veranda_time-entry] +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_alarm_minimum-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainwater_tank_level_alarm_minimum', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm minimum', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_minimum', + 'unique_id': 'tuya.fbya6s6rhaoyvl8hqgcwymini_set', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_alarm_minimum-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Rainwater Tank Level Alarm minimum', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.rainwater_tank_level_alarm_minimum', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_installation_height-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 3.0, + 'min': 0.1, + 'mode': , + 'step': 0.001, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainwater_tank_level_installation_height', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Installation height', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'installation_height', + 'unique_id': 'tuya.fbya6s6rhaoyvl8hqgcwyinstallation_height', + 'unit_of_measurement': 'm', + }) +# --- +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_installation_height-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Rainwater Tank Level Installation height', + 'max': 3.0, + 'min': 0.1, + 'mode': , + 'step': 0.001, + 'unit_of_measurement': 'm', + }), + 'context': , + 'entity_id': 'number.rainwater_tank_level_installation_height', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.35', + }) +# --- +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_maximum_liquid_depth-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 2.7, + 'min': 0.1, + 'mode': , + 'step': 0.001, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.rainwater_tank_level_maximum_liquid_depth', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum liquid depth', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'maximum_liquid_depth', + 'unique_id': 'tuya.fbya6s6rhaoyvl8hqgcwyliquid_depth_max', + 'unit_of_measurement': 'm', + }) +# --- +# name: test_platform_setup_and_discovery[number.rainwater_tank_level_maximum_liquid_depth-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Rainwater Tank Level Maximum liquid depth', + 'max': 2.7, + 'min': 0.1, + 'mode': , + 'step': 0.001, + 'unit_of_measurement': 'm', + }), + 'context': , + 'entity_id': 'number.rainwater_tank_level_maximum_liquid_depth', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) +# --- +# name: test_platform_setup_and_discovery[number.siren_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.siren_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time', + 'unique_id': 'tuya.okwwus27jhqqe2mijbgsalarm_time', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.siren_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren Time', + 'max': 10.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.siren_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.siren_veranda_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -679,11 +1736,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'time', - 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_time', + 'unique_id': 'tuya.kjr0pqg7eunn4vlujbgsalarm_time', 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][number.siren_veranda_time-state] +# name: test_platform_setup_and_discovery[number.siren_veranda_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Siren veranda Time', @@ -701,13 +1758,129 @@ 'state': '10.0', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][number.c9_volume-entry] +# name: test_platform_setup_and_discovery[number.smart_thermostats_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'max': 10.0, + 'max': 9.0, + 'min': -9.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.smart_thermostats_temperature_correction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature correction', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temp_correction', + 'unique_id': 'tuya.sb3zdertrw50bgogkwtemp_correction', + 'unit_of_measurement': '摄氏度', + }) +# --- +# name: test_platform_setup_and_discovery[number.smart_thermostats_temperature_correction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'smart thermostats Temperature correction', + 'max': 9.0, + 'min': -9.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '摄氏度', + }), + 'context': , + 'entity_id': 'number.smart_thermostats_temperature_correction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-2.0', + }) +# --- +# name: test_platform_setup_and_discovery[number.sous_vide_cook_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 92.5, + 'min': 25.0, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.sous_vide_cook_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cook temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cook_temperature', + 'unique_id': 'tuya.hyda5jsihokacvaqjzmcook_temperature', + 'unit_of_measurement': '℃', + }) +# --- +# name: test_platform_setup_and_discovery[number.sous_vide_cook_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sous Vide Cook temperature', + 'max': 92.5, + 'min': 25.0, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '℃', + }), + 'context': , + 'entity_id': 'number.sous_vide_cook_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[number.sous_vide_cook_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 5999.0, 'min': 1.0, 'mode': , 'step': 1.0, @@ -719,7 +1892,65 @@ 'disabled_by': None, 'domain': 'number', 'entity_category': , - 'entity_id': 'number.c9_volume', + 'entity_id': 'number.sous_vide_cook_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cook time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cook_time', + 'unique_id': 'tuya.hyda5jsihokacvaqjzmcook_time', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[number.sous_vide_cook_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sous Vide Cook time', + 'max': 5999.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.sous_vide_cook_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[number.v20_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.v20_volume', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -737,29 +1968,29 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfbasic_device_volume', - 'unit_of_measurement': '', + 'unique_id': 'tuya.zrrraytdoanz33rldsvolume_set', + 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][number.c9_volume-state] +# name: test_platform_setup_and_discovery[number.v20_volume-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'C9 Volume', - 'max': 10.0, - 'min': 1.0, + 'friendly_name': 'V20 Volume', + 'max': 100.0, + 'min': 0.0, 'mode': , 'step': 1.0, - 'unit_of_measurement': '', + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'number.c9_volume', + 'entity_id': 'number.v20_volume', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.0', + 'state': '95.0', }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] +# name: test_platform_setup_and_discovery[number.wifi_smart_gas_boiler_thermostat_temperature_correction-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -795,11 +2026,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_correction', - 'unique_id': 'tuya.bfb45cb8a9452fba66lexgtemp_correction', + 'unique_id': 'tuya.j6mn1t4ut5end6ifkwtemp_correction', 'unit_of_measurement': '℃', }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] +# name: test_platform_setup_and_discovery[number.wifi_smart_gas_boiler_thermostat_temperature_correction-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Temperature correction', @@ -817,471 +2048,3 @@ 'state': '-1.5', }) # --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_alarm_maximum-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.rainwater_tank_level_alarm_maximum', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Alarm maximum', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'alarm_maximum', - 'unique_id': 'tuya.bf3d16d38b17d7034ddxd4max_set', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_alarm_maximum-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Rainwater Tank Level Alarm maximum', - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'number.rainwater_tank_level_alarm_maximum', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '90.0', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_alarm_minimum-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.rainwater_tank_level_alarm_minimum', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Alarm minimum', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'alarm_minimum', - 'unique_id': 'tuya.bf3d16d38b17d7034ddxd4mini_set', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_alarm_minimum-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Rainwater Tank Level Alarm minimum', - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'number.rainwater_tank_level_alarm_minimum', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10.0', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_installation_height-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 3.0, - 'min': 0.1, - 'mode': , - 'step': 0.001, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.rainwater_tank_level_installation_height', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Installation height', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'installation_height', - 'unique_id': 'tuya.bf3d16d38b17d7034ddxd4installation_height', - 'unit_of_measurement': 'm', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_installation_height-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'distance', - 'friendly_name': 'Rainwater Tank Level Installation height', - 'max': 3.0, - 'min': 0.1, - 'mode': , - 'step': 0.001, - 'unit_of_measurement': 'm', - }), - 'context': , - 'entity_id': 'number.rainwater_tank_level_installation_height', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.35', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_maximum_liquid_depth-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 2.7, - 'min': 0.1, - 'mode': , - 'step': 0.001, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.rainwater_tank_level_maximum_liquid_depth', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Maximum liquid depth', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'maximum_liquid_depth', - 'unique_id': 'tuya.bf3d16d38b17d7034ddxd4liquid_depth_max', - 'unit_of_measurement': 'm', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][number.rainwater_tank_level_maximum_liquid_depth-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'distance', - 'friendly_name': 'Rainwater Tank Level Maximum liquid depth', - 'max': 2.7, - 'min': 0.1, - 'mode': , - 'step': 0.001, - 'unit_of_measurement': 'm', - }), - 'context': , - 'entity_id': 'number.rainwater_tank_level_maximum_liquid_depth', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.1', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_alarm_maximum-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.house_water_level_alarm_maximum', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Alarm maximum', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'alarm_maximum', - 'unique_id': 'tuya.mocked_device_idmax_set', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_alarm_maximum-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'House Water Level Alarm maximum', - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'number.house_water_level_alarm_maximum', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100.0', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_alarm_minimum-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.house_water_level_alarm_minimum', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Alarm minimum', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'alarm_minimum', - 'unique_id': 'tuya.mocked_device_idmini_set', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_alarm_minimum-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'House Water Level Alarm minimum', - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'number.house_water_level_alarm_minimum', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_installation_height-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 2.5, - 'min': 0.2, - 'mode': , - 'step': 0.001, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.house_water_level_installation_height', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Installation height', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'installation_height', - 'unique_id': 'tuya.mocked_device_idinstallation_height', - 'unit_of_measurement': 'm', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_installation_height-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'distance', - 'friendly_name': 'House Water Level Installation height', - 'max': 2.5, - 'min': 0.2, - 'mode': , - 'step': 0.001, - 'unit_of_measurement': 'm', - }), - 'context': , - 'entity_id': 'number.house_water_level_installation_height', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.56', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_maximum_liquid_depth-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 2.4, - 'min': 0.1, - 'mode': , - 'step': 0.001, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.house_water_level_maximum_liquid_depth', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Maximum liquid depth', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'maximum_liquid_depth', - 'unique_id': 'tuya.mocked_device_idliquid_depth_max', - 'unit_of_measurement': 'm', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][number.house_water_level_maximum_liquid_depth-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'distance', - 'friendly_name': 'House Water Level Maximum liquid depth', - 'max': 2.4, - 'min': 0.1, - 'mode': , - 'step': 0.001, - 'unit_of_measurement': 'm', - }), - 'context': , - 'entity_id': 'number.house_water_level_maximum_liquid_depth', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.1', - }) -# --- diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 98e3174b077..7c68a647040 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -1,13 +1,14 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_cpbo62rn][select.blinds_mode-entry] +# name: test_platform_setup_and_discovery[select.3dprinter_indicator_light_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - 'morning', - 'night', + 'relay', + 'pos', + 'none', ]), }), 'config_entry_id': , @@ -17,7 +18,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.blinds_mode', + 'entity_id': 'select.3dprinter_indicator_light_mode', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -29,42 +30,44 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Mode', + 'original_name': 'Indicator light mode', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'curtain_mode', - 'unique_id': 'tuya.bf216113c71bf01a18jtl0mode', + 'translation_key': 'light_mode', + 'unique_id': 'tuya.pykascx9yfqrxtbgzclight_mode', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_cpbo62rn][select.blinds_mode-state] +# name: test_platform_setup_and_discovery[select.3dprinter_indicator_light_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'blinds Mode', + 'friendly_name': '3DPrinter Indicator light mode', 'options': list([ - 'morning', - 'night', + 'relay', + 'pos', + 'none', ]), }), 'context': , - 'entity_id': 'select.blinds_mode', + 'entity_id': 'select.3dprinter_indicator_light_mode', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'morning', + 'state': 'relay', }) # --- -# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-entry] +# name: test_platform_setup_and_discovery[select.3dprinter_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - 'forward', - 'back', + 'power_off', + 'power_on', + 'last', ]), }), 'config_entry_id': , @@ -74,7 +77,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.kitchen_blinds_motor_mode', + 'entity_id': 'select.3dprinter_power_on_behavior', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -86,34 +89,153 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Motor mode', + 'original_name': 'Power on behavior', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'curtain_motor_mode', - 'unique_id': 'tuya.zah67ekdcontrol_back_mode', + 'translation_key': 'relay_status', + 'unique_id': 'tuya.pykascx9yfqrxtbgzcrelay_status', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cl_zah67ekd][select.kitchen_blinds_motor_mode-state] +# name: test_platform_setup_and_discovery[select.3dprinter_power_on_behavior-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Kitchen Blinds Motor mode', + 'friendly_name': '3DPrinter Power on behavior', 'options': list([ - 'forward', - 'back', + 'power_off', + 'power_on', + 'last', ]), }), 'context': , - 'entity_id': 'select.kitchen_blinds_motor_mode', + 'entity_id': 'select.3dprinter_power_on_behavior', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'forward', + 'state': 'last', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-entry] +# name: test_platform_setup_and_discovery[select.4_433_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.4_433_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.xenxir4a0tn0p1qcqdtrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.4_433_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '4-433 Power on behavior', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.4_433_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.6294ha_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.6294ha_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.q62sg0p3s52thp6zzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.6294ha_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '6294HA Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.6294ha_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- +# name: test_platform_setup_and_discovery[select.aqi_volume-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -151,11 +273,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinalarm_volume', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2ocalarm_volume', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][select.aqi_volume-state] +# name: test_platform_setup_and_discovery[select.aqi_volume-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Volume', @@ -174,7 +296,1476 @@ 'state': 'low', }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-entry] +# name: test_platform_setup_and_discovery[select.aubess_cooker_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aubess_cooker_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.cju47ovcbeuapei2zclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.aubess_cooker_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aubess Cooker Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.aubess_cooker_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.aubess_cooker_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aubess_cooker_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.cju47ovcbeuapei2zcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.aubess_cooker_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aubess Cooker Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.aubess_cooker_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.aubess_washing_machine_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aubess_washing_machine_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.zjh9xhtm3gibs9kizclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.aubess_washing_machine_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aubess Washing Machine Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.aubess_washing_machine_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'relay', + }) +# --- +# name: test_platform_setup_and_discovery[select.aubess_washing_machine_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aubess_washing_machine_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.zjh9xhtm3gibs9kizcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.aubess_washing_machine_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aubess Washing Machine Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.aubess_washing_machine_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'last', + }) +# --- +# name: test_platform_setup_and_discovery[select.balkonbewasserung_weather_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.balkonbewasserung_weather_delay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Weather delay', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather_delay', + 'unique_id': 'tuya.73ov8i8iedtylkzrqzkfsweather_delay', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.balkonbewasserung_weather_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'balkonbewässerung Weather delay', + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'context': , + 'entity_id': 'select.balkonbewasserung_weather_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cancel', + }) +# --- +# name: test_platform_setup_and_discovery[select.bathroom_light_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.bathroom_light_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.gluaktf5gklight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.bathroom_light_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'bathroom light Indicator light mode', + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'context': , + 'entity_id': 'select.bathroom_light_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pos', + }) +# --- +# name: test_platform_setup_and_discovery[select.bathroom_light_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.bathroom_light_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.gluaktf5gkrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.bathroom_light_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'bathroom light Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.bathroom_light_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_off', + }) +# --- +# name: test_platform_setup_and_discovery[select.blinds_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'morning', + 'night', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.blinds_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'curtain_mode', + 'unique_id': 'tuya.nr26obpclcmode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.blinds_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'blinds Mode', + 'options': list([ + 'morning', + 'night', + ]), + }), + 'context': , + 'entity_id': 'select.blinds_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'morning', + }) +# --- +# name: test_platform_setup_and_discovery[select.bree_countdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + '4h', + '5h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.bree_countdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Countdown', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'countdown', + 'unique_id': 'tuya.ppgdpsq1xaxlyzryjkcountdown_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.bree_countdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bree Countdown', + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + '4h', + '5h', + ]), + }), + 'context': , + 'entity_id': 'select.bree_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cancel', + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_anti_flicker-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.burocam_anti_flicker', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Anti-flicker', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'basic_anti_flicker', + 'unique_id': 'tuya.svjjuwykgijjedurpsbasic_anti_flicker', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_anti_flicker-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bürocam Anti-flicker', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.burocam_anti_flicker', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.burocam_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.svjjuwykgijjedurpsmotion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bürocam Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.burocam_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_night_vision-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.burocam_night_vision', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Night vision', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'basic_nightvision', + 'unique_id': 'tuya.svjjuwykgijjedurpsbasic_nightvision', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_night_vision-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bürocam Night vision', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.burocam_night_vision', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.burocam_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.svjjuwykgijjedurpsrecord_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bürocam Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.burocam_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_sound_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.burocam_sound_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'decibel_sensitivity', + 'unique_id': 'tuya.svjjuwykgijjedurpsdecibel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.burocam_sound_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bürocam Sound detection sensitivity', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.burocam_sound_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.c9_ipc_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.c9_ipc_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'IPC mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ipc_work_mode', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsipc_work_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.c9_ipc_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'C9 IPC mode', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.c9_ipc_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.c9_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.c9_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsmotion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.c9_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'C9 Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.c9_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.c9_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.c9_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsrecord_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.c9_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'C9 Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.c9_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.mgcpxpmovasazerdpsmotion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_night_vision-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_night_vision', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Night vision', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'basic_nightvision', + 'unique_id': 'tuya.mgcpxpmovasazerdpsbasic_nightvision', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_night_vision-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Night vision', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_night_vision', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.mgcpxpmovasazerdpsrecord_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_sound_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_garage_sound_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'decibel_sensitivity', + 'unique_id': 'tuya.mgcpxpmovasazerdpsdecibel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_garage_sound_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Sound detection sensitivity', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.cam_garage_sound_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_porch_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsmotion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_porch_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_porch_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsrecord_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_porch_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_porch_sound_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cam_porch_sound_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'decibel_sensitivity', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsdecibel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.cam_porch_sound_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Sound detection sensitivity', + 'options': list([ + '0', + '1', + ]), + }), + 'context': , + 'entity_id': 'select.cam_porch_sound_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.ceiling_fan_with_light_countdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '1h', + '2h', + '4h', + '8h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.ceiling_fan_with_light_countdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Countdown', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'countdown', + 'unique_id': 'tuya.ijzjlqwmv1blwe0gsfcountdown_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.ceiling_fan_with_light_countdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ceiling Fan With Light Countdown', + 'options': list([ + 'cancel', + '1h', + '2h', + '4h', + '8h', + ]), + }), + 'context': , + 'entity_id': 'select.ceiling_fan_with_light_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.dehumidifer_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -212,11 +1803,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'countdown', - 'unique_id': 'tuya.mock_device_idcountdown_set', + 'unique_id': 'tuya.ifzgvpgoodrfw2aksccountdown_set', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][select.dehumidifer_countdown-state] +# name: test_platform_setup_and_discovery[select.dehumidifer_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifer Countdown', @@ -235,7 +1826,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-entry] +# name: test_platform_setup_and_discovery[select.dehumidifier_countdown-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -273,11 +1864,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'countdown', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqcountdown_set', + 'unique_id': 'tuya.2myxayqtud9aqbizsccountdown_set', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][select.dehumidifier_countdown-state] +# name: test_platform_setup_and_discovery[select.dehumidifier_countdown-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Dehumidifier Countdown', @@ -296,7 +1887,1614 @@ 'state': 'cancel', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-entry] +# name: test_platform_setup_and_discovery[select.elivco_kitchen_socket_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.elivco_kitchen_socket_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.elivco_kitchen_socket_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Elivco Kitchen Socket Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.elivco_kitchen_socket_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'relay', + }) +# --- +# name: test_platform_setup_and_discovery[select.elivco_kitchen_socket_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.elivco_kitchen_socket_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.elivco_kitchen_socket_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Elivco Kitchen Socket Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.elivco_kitchen_socket_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- +# name: test_platform_setup_and_discovery[select.elivco_tv_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.elivco_tv_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.elivco_tv_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Elivco TV Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.elivco_tv_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'relay', + }) +# --- +# name: test_platform_setup_and_discovery[select.elivco_tv_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.elivco_tv_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.elivco_tv_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Elivco TV Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.elivco_tv_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'last', + }) +# --- +# name: test_platform_setup_and_discovery[select.framboisier_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.framboisier_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.51tdkcsamisw9ukycplight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.framboisier_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Framboisier Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.framboisier_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_platform_setup_and_discovery[select.framboisier_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.framboisier_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.51tdkcsamisw9ukycprelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.framboisier_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Framboisier Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.framboisier_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- +# name: test_platform_setup_and_discovery[select.framboisiers_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.framboisiers_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.vrhdtr5fawoiyth9qdtrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.framboisiers_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Framboisiers Power on behavior', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.framboisiers_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.garage_camera_motion_detection_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garage_camera_motion_detection_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection sensitivity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_sensitivity', + 'unique_id': 'tuya.53fnjncm3jywuaznpsmotion_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.garage_camera_motion_detection_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Motion detection sensitivity', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.garage_camera_motion_detection_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.garage_camera_record_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garage_camera_record_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Record mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'record_mode', + 'unique_id': 'tuya.53fnjncm3jywuaznpsrecord_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.garage_camera_record_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Record mode', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.garage_camera_record_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.garden_valve_yard_weather_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_valve_yard_weather_delay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Weather delay', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather_delay', + 'unique_id': 'tuya.ggimpv4dqzkfsweather_delay', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.garden_valve_yard_weather_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Valve Yard Weather delay', + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'context': , + 'entity_id': 'select.garden_valve_yard_weather_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cancel', + }) +# --- +# name: test_platform_setup_and_discovery[select.hoover_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'random', + 'smart', + 'wall_follow', + 'chargego', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.hoover_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'vacuum_mode', + 'unique_id': 'tuya.mwsaod7fa3gjyh6idsmode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.hoover_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hoover Mode', + 'options': list([ + 'random', + 'smart', + 'wall_follow', + 'chargego', + ]), + }), + 'context': , + 'entity_id': 'select.hoover_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'chargego', + }) +# --- +# name: test_platform_setup_and_discovery[select.ineox_sp2_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.ineox_sp2_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.vx2owjsg86g2ys93zcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.ineox_sp2_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ineox SP2 Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.ineox_sp2_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'last', + }) +# --- +# name: test_platform_setup_and_discovery[select.ion1000pro_countdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + '3', + '4', + '5', + '6', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.ion1000pro_countdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Countdown', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'countdown', + 'unique_id': 'tuya.owozxdzgbibizu4sjkcountdown_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.ion1000pro_countdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO Countdown', + 'options': list([ + '1', + '2', + '3', + '4', + '5', + '6', + ]), + }), + 'context': , + 'entity_id': 'select.ion1000pro_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.jardin_fraises_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.jardin_fraises_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.b6e05dfy4qhpgea1qdtrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.jardin_fraises_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'jardin Fraises Power on behavior', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.jardin_fraises_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[select.kalado_air_purifier_countdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + '4h', + '5h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.kalado_air_purifier_countdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Countdown', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'countdown', + 'unique_id': 'tuya.yo2karkjuhzztxsfjkcountdown_set', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.kalado_air_purifier_countdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kalado Air Purifier Countdown', + 'options': list([ + 'cancel', + '1h', + '2h', + '3h', + '4h', + '5h', + ]), + }), + 'context': , + 'entity_id': 'select.kalado_air_purifier_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cancel', + }) +# --- +# name: test_platform_setup_and_discovery[select.kitchen_blinds_motor_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'forward', + 'back', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.kitchen_blinds_motor_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motor mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'curtain_motor_mode', + 'unique_id': 'tuya.dke76hazlccontrol_back_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.kitchen_blinds_motor_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kitchen Blinds Motor mode', + 'options': list([ + 'forward', + 'back', + ]), + }), + 'context': , + 'entity_id': 'select.kitchen_blinds_motor_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'forward', + }) +# --- +# name: test_platform_setup_and_discovery[select.lave_linge_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.lave_linge_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.g0edqq0wzclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.lave_linge_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Lave linge Indicator light mode', + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'context': , + 'entity_id': 'select.lave_linge_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_platform_setup_and_discovery[select.lave_linge_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.lave_linge_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.g0edqq0wzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.lave_linge_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Lave linge Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.lave_linge_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- +# name: test_platform_setup_and_discovery[select.office_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.office_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.2x473nefusdo7af6zclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.office_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Office Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.office_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'relay', + }) +# --- +# name: test_platform_setup_and_discovery[select.office_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.office_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.2x473nefusdo7af6zcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.office_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Office Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.office_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'last', + }) +# --- +# name: test_platform_setup_and_discovery[select.persiana_do_quarto_motor_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'forward', + 'back', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.persiana_do_quarto_motor_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motor mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'curtain_motor_mode', + 'unique_id': 'tuya.ikbbdbnqsd70pc1glccontrol_back_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.persiana_do_quarto_motor_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Persiana do Quarto Motor mode', + 'options': list([ + 'forward', + 'back', + ]), + }), + 'context': , + 'entity_id': 'select.persiana_do_quarto_motor_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'back', + }) +# --- +# name: test_platform_setup_and_discovery[select.raspy4_home_assistant_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.raspy4_home_assistant_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.zaszonjgzclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.raspy4_home_assistant_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Raspy4 - Home Assistant Indicator light mode', + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'context': , + 'entity_id': 'select.raspy4_home_assistant_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_platform_setup_and_discovery[select.raspy4_home_assistant_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.raspy4_home_assistant_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.zaszonjgzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.raspy4_home_assistant_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Raspy4 - Home Assistant Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.raspy4_home_assistant_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- +# name: test_platform_setup_and_discovery[select.security_light_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.security_light_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.bxfkpxjgux2fgwnazclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.security_light_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Security Light Indicator light mode', + 'options': list([ + 'none', + 'relay', + 'pos', + ]), + }), + 'context': , + 'entity_id': 'select.security_light_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'relay', + }) +# --- +# name: test_platform_setup_and_discovery[select.security_light_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.security_light_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.bxfkpxjgux2fgwnazcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.security_light_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Security Light Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.security_light_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- +# name: test_platform_setup_and_discovery[select.siren_veranda_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.siren_veranda_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.kjr0pqg7eunn4vlujbgsalarm_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.siren_veranda_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda Volume', + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'context': , + 'entity_id': 'select.siren_veranda_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'middle', + }) +# --- +# name: test_platform_setup_and_discovery[select.siren_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.siren_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.okwwus27jhqqe2mijbgsalarm_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.siren_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren Volume', + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'context': , + 'entity_id': 'select.siren_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[select.smart_odor_eliminator_pro_odor_elimination_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -332,11 +3530,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'odor_elimination_mode', - 'unique_id': 'tuya.bf6574iutyikgwkxwork_mode', + 'unique_id': 'tuya.rl39uwgaqwjwcwork_mode', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][select.smart_odor_eliminator_pro_odor_elimination_mode-state] +# name: test_platform_setup_and_discovery[select.smart_odor_eliminator_pro_odor_elimination_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smart Odor Eliminator-Pro Odor elimination mode', @@ -353,16 +3551,17 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_indicator_light_mode-entry] +# name: test_platform_setup_and_discovery[select.smart_water_timer_weather_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - 'relay', - 'pos', - 'none', + 'cancel', + '24h', + '48h', + '72h', ]), }), 'config_entry_id': , @@ -372,7 +3571,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'entity_id': 'select.smart_water_timer_weather_delay', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -384,44 +3583,45 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Indicator light mode', + 'original_name': 'Weather delay', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'light_mode', - 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2elight_mode', + 'translation_key': 'weather_delay', + 'unique_id': 'tuya.bl5cuqxnqzkfsweather_delay', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_indicator_light_mode-state] +# name: test_platform_setup_and_discovery[select.smart_water_timer_weather_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'wallwasher front Indicator light mode', + 'friendly_name': 'Smart Water Timer Weather delay', 'options': list([ - 'relay', - 'pos', - 'none', + 'cancel', + '24h', + '48h', + '72h', ]), }), 'context': , - 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'entity_id': 'select.smart_water_timer_weather_delay', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_power_on_behavior-entry] +# name: test_platform_setup_and_discovery[select.socket3_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - 'power_off', - 'power_on', - 'last', + '0', + '1', + '2', ]), }), 'config_entry_id': , @@ -431,7 +3631,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.wallwasher_front_power_on_behavior', + 'entity_id': 'select.socket3_power_on_behavior', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -449,14 +3649,132 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_status', - 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2erelay_status', + 'unique_id': 'tuya.7zogt3pcwhxhu8upqdtrelay_status', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][select.wallwasher_front_power_on_behavior-state] +# name: test_platform_setup_and_discovery[select.socket3_power_on_behavior-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'wallwasher front Power on behavior', + 'friendly_name': 'Socket3 Power on behavior', + 'options': list([ + '0', + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'select.socket3_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.socket4_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.socket4_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.4q5c2am8n1bwb6bszclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.socket4_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Socket4 Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.socket4_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.socket4_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.socket4_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.4q5c2am8n1bwb6bszcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.socket4_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Socket4 Power on behavior', 'options': list([ 'power_off', 'power_on', @@ -464,25 +3782,23 @@ ]), }), 'context': , - 'entity_id': 'select.wallwasher_front_power_on_behavior', + 'entity_id': 'select.socket4_power_on_behavior', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][select.ceiling_fan_with_light_countdown-entry] +# name: test_platform_setup_and_discovery[select.spot_1_indicator_light_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - 'cancel', - '1h', - '2h', - '4h', - '8h', + 'relay', + 'pos', + 'none', ]), }), 'config_entry_id': , @@ -492,7 +3808,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.ceiling_fan_with_light_countdown', + 'entity_id': 'select.spot_1_indicator_light_mode', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -504,49 +3820,44 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Countdown', + 'original_name': 'Indicator light mode', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'countdown', - 'unique_id': 'tuya.XXXcountdown_set', + 'translation_key': 'light_mode', + 'unique_id': 'tuya.kffnst1epj6vr8xnzclight_mode', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[fs_g0ewlb1vmwqljzji][select.ceiling_fan_with_light_countdown-state] +# name: test_platform_setup_and_discovery[select.spot_1_indicator_light_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Ceiling Fan With Light Countdown', + 'friendly_name': 'Spot 1 Indicator light mode', 'options': list([ - 'cancel', - '1h', - '2h', - '4h', - '8h', + 'relay', + 'pos', + 'none', ]), }), 'context': , - 'entity_id': 'select.ceiling_fan_with_light_countdown', + 'entity_id': 'select.spot_1_indicator_light_mode', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'relay', }) # --- -# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-entry] +# name: test_platform_setup_and_discovery[select.spot_1_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - 'cancel', - '1h', - '2h', - '3h', - '4h', - '5h', + 'power_off', + 'power_on', + 'last', ]), }), 'config_entry_id': , @@ -556,7 +3867,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.bree_countdown', + 'entity_id': 'select.spot_1_power_on_behavior', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -568,38 +3879,257 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Countdown', + 'original_name': 'Power on behavior', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'countdown', - 'unique_id': 'tuya.CENSOREDcountdown_set', + 'translation_key': 'relay_status', + 'unique_id': 'tuya.kffnst1epj6vr8xnzcrelay_status', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][select.bree_countdown-state] +# name: test_platform_setup_and_discovery[select.spot_1_power_on_behavior-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bree Countdown', + 'friendly_name': 'Spot 1 Power on behavior', 'options': list([ - 'cancel', - '1h', - '2h', - '3h', - '4h', - '5h', + 'power_off', + 'power_on', + 'last', ]), }), 'context': , - 'entity_id': 'select.bree_countdown', + 'entity_id': 'select.spot_1_power_on_behavior', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'cancel', + 'state': 'last', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][select.v20_mode-entry] +# name: test_platform_setup_and_discovery[select.sunbeam_bedding_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'level_1', + 'level_2', + 'level_3', + 'level_4', + 'level_5', + 'level_6', + 'level_7', + 'level_8', + 'level_9', + 'level_10', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.sunbeam_bedding_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:thermometer-lines', + 'original_name': 'Level', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'blanket_level', + 'unique_id': 'tuya.fasvixqysw1lxvjprdlevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.sunbeam_bedding_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sunbeam Bedding Level', + 'icon': 'mdi:thermometer-lines', + 'options': list([ + 'level_1', + 'level_2', + 'level_3', + 'level_4', + 'level_5', + 'level_6', + 'level_7', + 'level_8', + 'level_9', + 'level_10', + ]), + }), + 'context': , + 'entity_id': 'select.sunbeam_bedding_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'level_5', + }) +# --- +# name: test_platform_setup_and_discovery[select.sunbeam_bedding_side_a_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'level_1', + 'level_2', + 'level_3', + 'level_4', + 'level_5', + 'level_6', + 'level_7', + 'level_8', + 'level_9', + 'level_10', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.sunbeam_bedding_side_a_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:thermometer-lines', + 'original_name': 'Side A Level', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'blanket_level', + 'unique_id': 'tuya.fasvixqysw1lxvjprdlevel_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.sunbeam_bedding_side_a_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sunbeam Bedding Side A Level', + 'icon': 'mdi:thermometer-lines', + 'options': list([ + 'level_1', + 'level_2', + 'level_3', + 'level_4', + 'level_5', + 'level_6', + 'level_7', + 'level_8', + 'level_9', + 'level_10', + ]), + }), + 'context': , + 'entity_id': 'select.sunbeam_bedding_side_a_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'level_5', + }) +# --- +# name: test_platform_setup_and_discovery[select.sunbeam_bedding_side_b_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'level_1', + 'level_2', + 'level_3', + 'level_4', + 'level_5', + 'level_6', + 'level_7', + 'level_8', + 'level_9', + 'level_10', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.sunbeam_bedding_side_b_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:thermometer-lines', + 'original_name': 'Side B Level', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'blanket_level', + 'unique_id': 'tuya.fasvixqysw1lxvjprdlevel_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.sunbeam_bedding_side_b_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sunbeam Bedding Side B Level', + 'icon': 'mdi:thermometer-lines', + 'options': list([ + 'level_1', + 'level_2', + 'level_3', + 'level_4', + 'level_5', + 'level_6', + 'level_7', + 'level_8', + 'level_9', + 'level_10', + ]), + }), + 'context': , + 'entity_id': 'select.sunbeam_bedding_side_b_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'level_5', + }) +# --- +# name: test_platform_setup_and_discovery[select.v20_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -637,11 +4167,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vacuum_mode', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtmode', + 'unique_id': 'tuya.zrrraytdoanz33rldsmode', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][select.v20_mode-state] +# name: test_platform_setup_and_discovery[select.v20_mode-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'V20 Mode', @@ -660,7 +4190,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][select.v20_water_tank_adjustment-entry] +# name: test_platform_setup_and_discovery[select.v20_water_tank_adjustment-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -697,11 +4227,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vacuum_cistern', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtcistern', + 'unique_id': 'tuya.zrrraytdoanz33rldscistern', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][select.v20_water_tank_adjustment-state] +# name: test_platform_setup_and_discovery[select.v20_water_tank_adjustment-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'V20 Water tank adjustment', @@ -719,17 +4249,17 @@ 'state': 'middle', }) # --- -# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][select.siren_veranda_volume-entry] +# name: test_platform_setup_and_discovery[select.valve_controller_2_weather_delay-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - 'low', - 'middle', - 'high', - 'mute', + 'cancel', + '24h', + '48h', + '72h', ]), }), 'config_entry_id': , @@ -739,7 +4269,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.siren_veranda_volume', + 'entity_id': 'select.valve_controller_2_weather_delay', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -751,800 +4281,44 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Volume', + 'original_name': 'Weather delay', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'volume', - 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_volume', + 'translation_key': 'weather_delay', + 'unique_id': 'tuya.kx8dncf1qzkfsweather_delay', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][select.siren_veranda_volume-state] +# name: test_platform_setup_and_discovery[select.valve_controller_2_weather_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Siren veranda Volume', + 'friendly_name': 'Valve Controller 2 Weather delay', 'options': list([ - 'low', - 'middle', - 'high', - 'mute', + 'cancel', + '24h', + '48h', + '72h', ]), }), 'context': , - 'entity_id': 'select.siren_veranda_volume', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'middle', - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_motion_detection_sensitivity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.cam_garage_motion_detection_sensitivity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Motion detection sensitivity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'motion_sensitivity', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmmotion_sensitivity', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_motion_detection_sensitivity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Motion detection sensitivity', - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.cam_garage_motion_detection_sensitivity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_night_vision-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.cam_garage_night_vision', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Night vision', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'basic_nightvision', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_nightvision', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_night_vision-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Night vision', - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.cam_garage_night_vision', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_record_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.cam_garage_record_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Record mode', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'record_mode', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmrecord_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_record_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Record mode', - 'options': list([ - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.cam_garage_record_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_sound_detection_sensitivity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.cam_garage_sound_detection_sensitivity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sound detection sensitivity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'decibel_sensitivity', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmdecibel_sensitivity', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][select.cam_garage_sound_detection_sensitivity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Sound detection sensitivity', - 'options': list([ - '0', - '1', - ]), - }), - 'context': , - 'entity_id': 'select.cam_garage_sound_detection_sensitivity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_motion_detection_sensitivity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.cam_porch_motion_detection_sensitivity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Motion detection sensitivity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'motion_sensitivity', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9motion_sensitivity', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_motion_detection_sensitivity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Motion detection sensitivity', - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.cam_porch_motion_detection_sensitivity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_record_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.cam_porch_record_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Record mode', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'record_mode', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9record_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_record_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Record mode', - 'options': list([ - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.cam_porch_record_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_sound_detection_sensitivity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.cam_porch_sound_detection_sensitivity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sound detection sensitivity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'decibel_sensitivity', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9decibel_sensitivity', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][select.cam_porch_sound_detection_sensitivity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Sound detection sensitivity', - 'options': list([ - '0', - '1', - ]), - }), - 'context': , - 'entity_id': 'select.cam_porch_sound_detection_sensitivity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][select.c9_ipc_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.c9_ipc_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'IPC mode', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'ipc_work_mode', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfipc_work_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][select.c9_ipc_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'C9 IPC mode', - 'options': list([ - '0', - '1', - ]), - }), - 'context': , - 'entity_id': 'select.c9_ipc_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][select.c9_motion_detection_sensitivity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.c9_motion_detection_sensitivity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Motion detection sensitivity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'motion_sensitivity', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfmotion_sensitivity', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][select.c9_motion_detection_sensitivity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'C9 Motion detection sensitivity', - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.c9_motion_detection_sensitivity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][select.c9_record_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.c9_record_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Record mode', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'record_mode', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfrecord_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][select.c9_record_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'C9 Record mode', - 'options': list([ - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.c9_record_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[tdq_1aegphq4yfd50e6b][select.jardin_fraises_power_on_behavior-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.jardin_fraises_power_on_behavior', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Power on behavior', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'relay_status', - 'unique_id': 'tuya.bfa008a4f82a56616c69uzrelay_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[tdq_1aegphq4yfd50e6b][select.jardin_fraises_power_on_behavior-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'jardin Fraises Power on behavior', - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.jardin_fraises_power_on_behavior', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[tdq_9htyiowaf5rtdhrv][select.framboisiers_power_on_behavior-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.framboisiers_power_on_behavior', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Power on behavior', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'relay_status', - 'unique_id': 'tuya.bff35871a2f4430058vs8urelay_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[tdq_9htyiowaf5rtdhrv][select.framboisiers_power_on_behavior-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Framboisiers Power on behavior', - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.framboisiers_power_on_behavior', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.4_433_power_on_behavior', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Power on behavior', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'relay_status', - 'unique_id': 'tuya.bf082711d275c0c883vb4prelay_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][select.4_433_power_on_behavior-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': '4-433 Power on behavior', - 'options': list([ - '0', - '1', - '2', - ]), - }), - 'context': , - 'entity_id': 'select.4_433_power_on_behavior', + 'entity_id': 'select.valve_controller_2_weather_delay', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][select.socket3_power_on_behavior-entry] +# name: test_platform_setup_and_discovery[select.vividstorm_screen_motor_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ - '0', - '1', - '2', + 'forward', + 'back', ]), }), 'config_entry_id': , @@ -1554,7 +4328,124 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.socket3_power_on_behavior', + 'entity_id': 'select.vividstorm_screen_motor_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motor mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'curtain_motor_mode', + 'unique_id': 'tuya.4hbnivc4w2rsw966lccontrol_back_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.vividstorm_screen_motor_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'VIVIDSTORM SCREEN Motor mode', + 'options': list([ + 'forward', + 'back', + ]), + }), + 'context': , + 'entity_id': 'select.vividstorm_screen_motor_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'forward', + }) +# --- +# name: test_platform_setup_and_discovery[select.wallwasher_front_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.pdasfna8fswh4a0tzclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.wallwasher_front_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.wallwasher_front_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.wallwasher_front_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wallwasher_front_power_on_behavior', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1572,25 +4463,202 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_status', - 'unique_id': 'tuya.bf0dc19ab84dc3627ep2unrelay_status', + 'unique_id': 'tuya.pdasfna8fswh4a0tzcrelay_status', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][select.socket3_power_on_behavior-state] +# name: test_platform_setup_and_discovery[select.wallwasher_front_power_on_behavior-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Socket3 Power on behavior', + 'friendly_name': 'wallwasher front Power on behavior', 'options': list([ - '0', - '1', - '2', + 'power_off', + 'power_on', + 'last', ]), }), 'context': , - 'entity_id': 'select.socket3_power_on_behavior', + 'entity_id': 'select.wallwasher_front_power_on_behavior', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[select.weihnachtsmann_indicator_light_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.weihnachtsmann_indicator_light_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indicator light mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'light_mode', + 'unique_id': 'tuya.rwp6kdezm97s2nktzclight_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.weihnachtsmann_indicator_light_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Weihnachtsmann Indicator light mode', + 'options': list([ + 'relay', + 'pos', + 'none', + ]), + }), + 'context': , + 'entity_id': 'select.weihnachtsmann_indicator_light_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.weihnachtsmann_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.weihnachtsmann_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.rwp6kdezm97s2nktzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.weihnachtsmann_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Weihnachtsmann Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.weihnachtsmann_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[select.wi_fi_hub_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wi_fi_hub_power_on_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.zspc4q1ut7swycnyzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.wi_fi_hub_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wi-Fi hub Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.wi_fi_hub_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index fade1fcbc2b..6c11d6034b8 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -1,18 +1,20 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_3r8gc33pnqsxfe1g][sensor.lounge_dark_blind_last_operation_duration-entry] +# name: test_platform_setup_and_discovery[sensor.3dprinter_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.lounge_dark_blind_last_operation_duration', + 'entity_category': None, + 'entity_id': 'sensor.3dprinter_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -21,34 +23,331 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Last operation duration', + 'original_name': 'Current', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'last_operation_duration', - 'unique_id': 'tuya.mocked_device_idtime_total', - 'unit_of_measurement': 'ms', + 'translation_key': 'current', + 'unique_id': 'tuya.pykascx9yfqrxtbgzccur_current', + 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cl_3r8gc33pnqsxfe1g][sensor.lounge_dark_blind_last_operation_duration-state] +# name: test_platform_setup_and_discovery[sensor.3dprinter_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Lounge Dark Blind Last operation duration', - 'unit_of_measurement': 'ms', + 'device_class': 'current', + 'friendly_name': '3DPrinter Current', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.lounge_dark_blind_last_operation_duration', + 'entity_id': 'sensor.3dprinter_current', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '25400.0', + 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-entry] +# name: test_platform_setup_and_discovery[sensor.3dprinter_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.3dprinter_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.pykascx9yfqrxtbgzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.3dprinter_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': '3DPrinter Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.3dprinter_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.3dprinter_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.3dprinter_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.pykascx9yfqrxtbgzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.3dprinter_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': '3DPrinter Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.3dprinter_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '231.9', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.6294ha_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.6294ha_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.q62sg0p3s52thp6zzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.6294ha_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': '6294HA Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.6294ha_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.466', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.6294ha_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.6294ha_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.q62sg0p3s52thp6zzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.6294ha_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': '6294HA Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.6294ha_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11374.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.6294ha_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.6294ha_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.q62sg0p3s52thp6zzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.6294ha_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': '6294HA Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.6294ha_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '239.6', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aqi_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -81,11 +380,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinbattery_percentage', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2ocbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_battery-state] +# name: test_platform_setup_and_discovery[sensor.aqi_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -101,7 +400,7 @@ 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-entry] +# name: test_platform_setup_and_discovery[sensor.aqi_formaldehyde-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -134,11 +433,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'formaldehyde', - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinch2o_value', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2occh2o_value', 'unit_of_measurement': 'mg/m3', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_formaldehyde-state] +# name: test_platform_setup_and_discovery[sensor.aqi_formaldehyde-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI Formaldehyde', @@ -153,7 +452,7 @@ 'state': '0.002', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-entry] +# name: test_platform_setup_and_discovery[sensor.aqi_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -186,11 +485,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinhumidity_value', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2ochumidity_value', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_humidity-state] +# name: test_platform_setup_and_discovery[sensor.aqi_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -206,7 +505,7 @@ 'state': '53.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-entry] +# name: test_platform_setup_and_discovery[sensor.aqi_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -242,11 +541,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpintemp_current', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2octemp_current', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_temperature-state] +# name: test_platform_setup_and_discovery[sensor.aqi_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -262,7 +561,7 @@ 'state': '26.0', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-entry] +# name: test_platform_setup_and_discovery[sensor.aqi_volatile_organic_compounds-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -295,11 +594,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voc', - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinvoc_value', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2ocvoc_value', 'unit_of_measurement': 'mg/m³', }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][sensor.aqi_volatile_organic_compounds-state] +# name: test_platform_setup_and_discovery[sensor.aqi_volatile_organic_compounds-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'volatile_organic_compounds', @@ -315,7 +614,7 @@ 'state': '0.018', }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-entry] +# name: test_platform_setup_and_discovery[sensor.aubess_cooker_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -330,7 +629,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.dehumidifer_humidity', + 'entity_id': 'sensor.aubess_cooker_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -339,36 +638,42 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Humidity', + 'original_name': 'Current', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.mock_device_idhumidity_indoor', - 'unit_of_measurement': '%', + 'translation_key': 'current', + 'unique_id': 'tuya.cju47ovcbeuapei2zccur_current', + 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][sensor.dehumidifer_humidity-state] +# name: test_platform_setup_and_discovery[sensor.aubess_cooker_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Dehumidifer Humidity', + 'device_class': 'current', + 'friendly_name': 'Aubess Cooker Current', 'state_class': , - 'unit_of_measurement': '%', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.dehumidifer_humidity', + 'entity_id': 'sensor.aubess_cooker_current', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-entry] +# name: test_platform_setup_and_discovery[sensor.aubess_cooker_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -383,7 +688,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.dehumidifier_humidity', + 'entity_id': 'sensor.aubess_cooker_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -392,36 +697,272 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Humidity', + 'original_name': 'Power', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqhumidity_indoor', - 'unit_of_measurement': '%', + 'translation_key': 'power', + 'unique_id': 'tuya.cju47ovcbeuapei2zccur_power', + 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][sensor.dehumidifier_humidity-state] +# name: test_platform_setup_and_discovery[sensor.aubess_cooker_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Dehumidifier Humidity', + 'device_class': 'power', + 'friendly_name': 'Aubess Cooker Power', 'state_class': , - 'unit_of_measurement': '%', + 'unit_of_measurement': 'W', }), 'context': , - 'entity_id': 'sensor.dehumidifier_humidity', + 'entity_id': 'sensor.aubess_cooker_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '47.0', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-entry] +# name: test_platform_setup_and_discovery[sensor.aubess_cooker_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aubess_cooker_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.cju47ovcbeuapei2zccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aubess_cooker_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Aubess Cooker Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.aubess_cooker_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aubess_washing_machine_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aubess_washing_machine_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.zjh9xhtm3gibs9kizccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aubess_washing_machine_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Aubess Washing Machine Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.aubess_washing_machine_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aubess_washing_machine_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aubess_washing_machine_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.zjh9xhtm3gibs9kizccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aubess_washing_machine_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Aubess Washing Machine Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.aubess_washing_machine_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aubess_washing_machine_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aubess_washing_machine_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.zjh9xhtm3gibs9kizccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.aubess_washing_machine_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Aubess Washing Machine Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.aubess_washing_machine_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '229.9', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.balkonbewasserung_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -436,7 +977,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.smart_odor_eliminator_pro_battery', + 'entity_id': 'sensor.balkonbewasserung_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -454,1425 +995,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.bf6574iutyikgwkxbattery_percentage', + 'unique_id': 'tuya.73ov8i8iedtylkzrqzkfsbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_battery-state] +# name: test_platform_setup_and_discovery[sensor.balkonbewasserung_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Smart Odor Eliminator-Pro Battery', + 'friendly_name': 'balkonbewässerung Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.smart_odor_eliminator_pro_battery', + 'entity_id': 'sensor.balkonbewasserung_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '90.0', }) # --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.smart_odor_eliminator_pro_status', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Status', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'odor_elimination_status', - 'unique_id': 'tuya.bf6574iutyikgwkxwork_state_e', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][sensor.smart_odor_eliminator_pro_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Smart Odor Eliminator-Pro Status', - }), - 'context': , - 'entity_id': 'sensor.smart_odor_eliminator_pro_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.cleverio_pf100_last_amount', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Last amount', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'last_amount', - 'unique_id': 'tuya.bfd0273e59494eb34esvrxfeed_report', - 'unit_of_measurement': '', - }) -# --- -# name: test_platform_setup_and_discovery[cwwsq_wfkzyy0evslzsmoi][sensor.cleverio_pf100_last_amount-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Cleverio PF100 Last amount', - 'state_class': , - 'unit_of_measurement': '', - }), - 'context': , - 'entity_id': 'sensor.cleverio_pf100_last_amount', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.0', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pixi_smart_drinking_fountain_filter_duration', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Filter duration', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'filter_duration', - 'unique_id': 'tuya.23536058083a8dc57d96filter_life', - 'unit_of_measurement': 'min', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_filter_duration-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'PIXI Smart Drinking Fountain Filter duration', - 'state_class': , - 'unit_of_measurement': 'min', - }), - 'context': , - 'entity_id': 'sensor.pixi_smart_drinking_fountain_filter_duration', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '18965.0', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pixi_smart_drinking_fountain_uv_runtime', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'UV runtime', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'uv_runtime', - 'unique_id': 'tuya.23536058083a8dc57d96uv_runtime', - 'unit_of_measurement': 's', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_uv_runtime-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'PIXI Smart Drinking Fountain UV runtime', - 'state_class': , - 'unit_of_measurement': 's', - }), - 'context': , - 'entity_id': 'sensor.pixi_smart_drinking_fountain_uv_runtime', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_level', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Water level', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'water_level_state', - 'unique_id': 'tuya.23536058083a8dc57d96water_level', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_level-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'PIXI Smart Drinking Fountain Water level', - }), - 'context': , - 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_level', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'level_3', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_pump_duration', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Water pump duration', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'pump_time', - 'unique_id': 'tuya.23536058083a8dc57d96pump_time', - 'unit_of_measurement': 'min', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_pump_duration-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'PIXI Smart Drinking Fountain Water pump duration', - 'state_class': , - 'unit_of_measurement': 'min', - }), - 'context': , - 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_pump_duration', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '18965.0', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_usage_duration', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Water usage duration', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'water_time', - 'unique_id': 'tuya.23536058083a8dc57d96water_time', - 'unit_of_measurement': 'min', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][sensor.pixi_smart_drinking_fountain_water_usage_duration-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'PIXI Smart Drinking Fountain Water usage duration', - 'state_class': , - 'unit_of_measurement': 'min', - }), - 'context': , - 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_usage_duration', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.hvac_meter_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'current', - 'unique_id': 'tuya.eb0c772dabbb19d653ssi5cur_current', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'HVAC Meter Current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.hvac_meter_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.083', - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.hvac_meter_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'power', - 'unique_id': 'tuya.eb0c772dabbb19d653ssi5cur_power', - 'unit_of_measurement': 'W', - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'HVAC Meter Power', - 'state_class': , - 'unit_of_measurement': 'W', - }), - 'context': , - 'entity_id': 'sensor.hvac_meter_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6.4', - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.hvac_meter_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Voltage', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'voltage', - 'unique_id': 'tuya.eb0c772dabbb19d653ssi5cur_voltage', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][sensor.hvac_meter_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'HVAC Meter Voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.hvac_meter_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '121.7', - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.droger_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'current', - 'unique_id': 'tuya.051724052462ab286504cur_current', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'droger Current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.droger_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.754', - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.droger_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'power', - 'unique_id': 'tuya.051724052462ab286504cur_power', - 'unit_of_measurement': 'W', - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'droger Power', - 'state_class': , - 'unit_of_measurement': 'W', - }), - 'context': , - 'entity_id': 'sensor.droger_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '593.5', - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.droger_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Voltage', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'voltage', - 'unique_id': 'tuya.051724052462ab286504cur_voltage', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][sensor.droger_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'droger Voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.droger_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '222.4', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'current', - 'unique_id': 'tuya.mocked_device_idcur_current', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': '一路带计量磁保持通断器 Current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.198', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'power', - 'unique_id': 'tuya.mocked_device_idcur_power', - 'unit_of_measurement': 'W', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': '一路带计量磁保持通断器 Power', - 'state_class': , - 'unit_of_measurement': 'W', - }), - 'context': , - 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '495.3', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Voltage', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'voltage', - 'unique_id': 'tuya.mocked_device_idcur_voltage', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': '一路带计量磁保持通断器 Voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '231.4', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase A current', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_a_current', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_aelectriccurrent', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase A current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.637', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase A power', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_a_power', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_apower', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase A power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.108', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase A voltage', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_a_voltage', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_avoltage', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_a_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase A voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '221.1', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase B current', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_b_current', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_belectriccurrent', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase B current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '11.203', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase B power', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_b_power', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_bpower', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase B power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '2.41', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase B voltage', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_b_voltage', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_bvoltage', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_b_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase B voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '218.7', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase C current', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_c_current', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_celectriccurrent', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_current-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase C current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_current', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.913', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase C power', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_c_power', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_cpower', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase C power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.092', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase C voltage', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'phase_c_voltage', - 'unique_id': 'tuya.bf5e5bde2c52cb5994cd27phase_cvoltage', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[dlq_kxdr6su0c55p7bbo][sensor.metering_3pn_wifi_stable_phase_c_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Metering_3PN_WiFi_stable Phase C voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '220.4', - }) -# --- -# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-entry] +# name: test_platform_setup_and_discovery[sensor.basement_temperature_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1887,7 +1030,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.door_garage_battery', + 'entity_id': 'sensor.basement_temperature_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1905,27 +1048,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.bf5cccf9027080e2dbb9w3battery', + 'unique_id': 'tuya.jlduh7vigcdswbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[mcs_7jIGJAymiH8OsFFb][sensor.door_garage_battery-state] +# name: test_platform_setup_and_discovery[sensor.basement_temperature_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Door Garage Battery', + 'friendly_name': 'Basement temperature Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.door_garage_battery', + 'entity_id': 'sensor.basement_temperature_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100.0', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][sensor.sous_vide_current_temperature-entry] +# name: test_platform_setup_and_discovery[sensor.basement_temperature_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1940,7 +1083,60 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.sous_vide_current_temperature', + 'entity_id': 'sensor.basement_temperature_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.jlduh7vigcdswva_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.basement_temperature_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Basement temperature Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.basement_temperature_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.basement_temperature_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.basement_temperature_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1955,178 +1151,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Current temperature', + 'original_name': 'Temperature', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'current_temperature', - 'unique_id': 'tuya.bff434eca843ffc9afmthvtemp_current', + 'translation_key': 'temperature', + 'unique_id': 'tuya.jlduh7vigcdswva_temperature', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][sensor.sous_vide_current_temperature-state] +# name: test_platform_setup_and_discovery[sensor.basement_temperature_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Sous Vide Current temperature', + 'friendly_name': 'Basement temperature Temperature', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.sous_vide_current_temperature', + 'entity_id': 'sensor.basement_temperature_temperature', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][sensor.sous_vide_remaining_time-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.sous_vide_remaining_time', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remaining time', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'remaining_time', - 'unique_id': 'tuya.bff434eca843ffc9afmthvremain_time', - 'unit_of_measurement': , - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][sensor.sous_vide_remaining_time-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sous Vide Remaining time', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.sous_vide_remaining_time', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][sensor.sous_vide_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.sous_vide_status', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Status', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'sous_vide_status', - 'unique_id': 'tuya.bff434eca843ffc9afmthvstatus', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][sensor.sous_vide_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sous Vide Status', - }), - 'context': , - 'entity_id': 'sensor.sous_vide_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][sensor.rat_trap_hedge_battery_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.rat_trap_hedge_battery_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Battery state', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'battery_state', - 'unique_id': 'tuya.73486068483fda10d633battery_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[pir_3amxzozho9xp4mkh][sensor.rat_trap_hedge_battery_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'rat trap hedge Battery state', - }), - 'context': , - 'entity_id': 'sensor.rat_trap_hedge_battery_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'low', - }) -# --- -# name: test_platform_setup_and_discovery[pir_fcdjzz3s][sensor.motion_sensor_lidl_zigbee_battery-entry] +# name: test_platform_setup_and_discovery[sensor.bassin_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2141,7 +1192,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', + 'entity_id': 'sensor.bassin_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2159,32 +1210,264 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.bf445324326cbde7c5rg7bbattery_percentage', + 'unique_id': 'tuya.iaagy4qigcdswbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[pir_fcdjzz3s][sensor.motion_sensor_lidl_zigbee_battery-state] +# name: test_platform_setup_and_discovery[sensor.bassin_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Motion sensor lidl zigbee Battery', + 'friendly_name': 'Bassin Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', + 'entity_id': 'sensor.bassin_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][sensor.pir_outside_stairs_battery_state-entry] +# name: test_platform_setup_and_discovery[sensor.bassin_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bassin_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.kvnsoqyfltmf0bknzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bassin_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Bassin Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bassin_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.783', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bassin_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bassin_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.kvnsoqyfltmf0bknzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bassin_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Bassin Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.bassin_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '41.1', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bassin_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bassin_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.iaagy4qigcdswva_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bassin_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Bassin Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bassin_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bassin_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bassin_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.kvnsoqyfltmf0bknzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bassin_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Bassin Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.bassin_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '245.4', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bathroom_smart_switch_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -2192,7 +1475,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.pir_outside_stairs_battery_state', + 'entity_id': 'sensor.bathroom_smart_switch_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2202,32 +1485,141 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery state', + 'original_name': 'Battery', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'battery_state', - 'unique_id': 'tuya.20401777500291cfe3a2battery_state', - 'unit_of_measurement': None, + 'translation_key': 'battery', + 'unique_id': 'tuya.fvywp3b5mu4zay8lgkxwbattery_percentage', + 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[pir_wqz93nrdomectyoz][sensor.pir_outside_stairs_battery_state-state] +# name: test_platform_setup_and_discovery[sensor.bathroom_smart_switch_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'PIR outside stairs Battery state', + 'device_class': 'battery', + 'friendly_name': 'Bathroom Smart Switch Battery', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.pir_outside_stairs_battery_state', + 'entity_id': 'sensor.bathroom_smart_switch_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'middle', + 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_air_pressure-entry] +# name: test_platform_setup_and_discovery[sensor.boite_aux_lettres_arriere_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.boite_aux_lettres_arriere_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.7obpyhy8scmbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.boite_aux_lettres_arriere_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Boîte aux lettres - arrière Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.boite_aux_lettres_arriere_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '62.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bouton_tempo_exterieur_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.bouton_tempo_exterieur_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.g5uso5ajgkxwbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.bouton_tempo_exterieur_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Bouton tempo extérieur Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.bouton_tempo_exterieur_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_air_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2263,11 +1655,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_pressure', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzatmospheric_pressture', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqatmospheric_pressture', 'unit_of_measurement': 'hPa', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_air_pressure-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_air_pressure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'pressure', @@ -2283,7 +1675,7 @@ 'state': '1004.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2314,11 +1706,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_state', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzbattery_state', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqbattery_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Battery state', @@ -2331,7 +1723,7 @@ 'state': 'high', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2364,11 +1756,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_value', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqhumidity_value', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2384,7 +1776,7 @@ 'state': '52.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2417,11 +1809,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'illuminance', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzbright_value', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqbright_value', 'unit_of_measurement': 'lx', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'illuminance', @@ -2437,7 +1829,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2470,11 +1862,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_outdoor', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_outdoor', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqhumidity_outdoor', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2490,7 +1882,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_1-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2523,11 +1915,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_humidity_outdoor', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_outdoor_1', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqhumidity_outdoor_1', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_1-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2543,7 +1935,7 @@ 'state': '99.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_2-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2576,11 +1968,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_humidity_outdoor', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_outdoor_2', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqhumidity_outdoor_2', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_2-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2596,7 +1988,7 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_3-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2629,11 +2021,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_humidity_outdoor', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_outdoor_3', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqhumidity_outdoor_3', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_3-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_outdoor_humidity_channel_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -2649,7 +2041,63 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_precipitation_intensity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_precipitation_intensity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Precipitation intensity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precipitation_intensity', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqrain_rate', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_precipitation_intensity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'precipitation_intensity', + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Precipitation intensity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_precipitation_intensity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2685,11 +2133,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_external', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current_external', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqtemp_current_external', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2705,7 +2153,7 @@ 'state': '-40.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_1-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2741,11 +2189,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_temperature_external', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current_external_1', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqtemp_current_external_1', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_1-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2761,7 +2209,7 @@ 'state': '19.3', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_2-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2797,11 +2245,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_temperature_external', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current_external_2', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqtemp_current_external_2', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_2-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2817,7 +2265,7 @@ 'state': '25.2', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_3-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2853,11 +2301,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_temperature_external', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current_external_3', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqtemp_current_external_3', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_3-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature_channel_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2873,7 +2321,7 @@ 'state': '-40.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2909,11 +2357,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqtemp_current', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -2929,7 +2377,166 @@ 'state': '24.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_speed-entry] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_total_precipitation_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_total_precipitation_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total precipitation today', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'precipitation_today', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqrain_24h', + 'unit_of_measurement': 'mm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_total_precipitation_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'precipitation', + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Total precipitation today', + 'state_class': , + 'unit_of_measurement': 'mm', + }), + 'context': , + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_total_precipitation_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_uv_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_uv_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV index', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_index', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxquv_index', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit UV index', + 'state_class': , + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_direction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_direction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wind direction', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wind_direction', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqwind_direct', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_direction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Wind direction', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_direction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2968,11 +2575,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed', - 'unique_id': 'tuya.bf84c743a84eb2c8abeurzwindspeed_avg', + 'unique_id': 'tuya.6tbtkuv3tal1aesfjxqwindspeed_avg', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_fsea1lat3vuktbt6][sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_speed-state] +# name: test_platform_setup_and_discovery[sensor.br_7_in_1_wlan_wetterstation_anthrazit_wind_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'wind_speed', @@ -2988,7 +2595,2152 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-entry] +# name: test_platform_setup_and_discovery[sensor.c9_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.c9_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspswireless_electricity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.c9_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'C9 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.c9_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.cleverio_pf100_last_amount-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.cleverio_pf100_last_amount', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Last amount', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_amount', + 'unique_id': 'tuya.iomszlsve0yyzkfwqswwcfeed_report', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.cleverio_pf100_last_amount-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Cleverio PF100 Last amount', + 'state_class': , + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'sensor.cleverio_pf100_last_amount', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.consommation_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.consommation_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.49m7h9lh3t8pq6ftzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.consommation_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Consommation Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.consommation_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.585', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.consommation_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.consommation_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.49m7h9lh3t8pq6ftzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.consommation_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Consommation Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.consommation_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '425.8', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.consommation_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.consommation_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.49m7h9lh3t8pq6ftzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.consommation_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Consommation Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.consommation_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '241.6', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.dehumidifer_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dehumidifer_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.ifzgvpgoodrfw2akschumidity_indoor', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.dehumidifer_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Dehumidifer Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dehumidifer_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.dehumidifier_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dehumidifier_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.2myxayqtud9aqbizschumidity_indoor', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.dehumidifier_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Dehumidifier Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dehumidifier_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '47.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.door_garage_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.door_garage_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.bFFsO8HimyAJGIj7scmbattery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.door_garage_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Door Garage Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.door_garage_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.droger_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.l8uxezzkc7c5a0jhzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.droger_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'droger Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.droger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.754', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.droger_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.l8uxezzkc7c5a0jhzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.droger_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'droger Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.droger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '593.5', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.droger_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.droger_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.l8uxezzkc7c5a0jhzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.droger_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'droger Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.droger_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '222.4', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_current', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_aelectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': '断路器HA Phase A current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '599.296', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_power', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_apower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': '断路器HA Phase A power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.432', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_voltage', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_avoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': '断路器HA Phase A voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_supply_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.duan_lu_qi_ha_supply_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Supply frequency', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'supply_frequency', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldsupply_frequency', + 'unit_of_measurement': 'Hz', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_supply_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': '断路器HA Supply frequency', + 'state_class': , + 'unit_of_measurement': 'Hz', + }), + 'context': , + 'entity_id': 'sensor.duan_lu_qi_ha_supply_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.eau_chaude_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.eau_chaude_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.a3qtb7pulkcc6jdjqldcur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.eau_chaude_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Eau Chaude Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.eau_chaude_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.067', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.eau_chaude_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.eau_chaude_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.a3qtb7pulkcc6jdjqldcur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.eau_chaude_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Eau Chaude Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.eau_chaude_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2441.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.eau_chaude_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.eau_chaude_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.a3qtb7pulkcc6jdjqldcur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.eau_chaude_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Eau Chaude Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.eau_chaude_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '241.9', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_a_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_current', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_aelectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Edesanya Energy Phase A current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_a_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.608', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_a_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_apower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Edesanya Energy Phase A power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_a_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.133', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_a_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_voltage', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_avoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_a_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Edesanya Energy Phase A voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_a_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '236.5', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_b_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_current', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_belectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Edesanya Energy Phase B current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_b_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_b_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bpower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Edesanya Energy Phase B power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_b_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_b_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_voltage', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bvoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Edesanya Energy Phase B voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_b_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_c_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_current', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_celectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Edesanya Energy Phase C current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_c_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_c_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cpower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Edesanya Energy Phase C power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_c_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_c_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_voltage', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cvoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Edesanya Energy Phase C voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_c_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldtotal_forward_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Edesanya Energy Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '219.72', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_kitchen_socket_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Elivco Kitchen Socket Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.elivco_kitchen_socket_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_kitchen_socket_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Elivco Kitchen Socket Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.elivco_kitchen_socket_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_kitchen_socket_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Elivco Kitchen Socket Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.elivco_kitchen_socket_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '233.4', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_tv_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Elivco TV Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.elivco_tv_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.091', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_tv_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Elivco TV Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.elivco_tv_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_tv_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Elivco TV Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.elivco_tv_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '237.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.fenetre_cuisine_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.fenetre_cuisine_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.yuanswy6scmbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.fenetre_cuisine_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Fenêtre cuisine Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.fenetre_cuisine_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '93.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.framboisier_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.framboisier_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.51tdkcsamisw9ukycpcur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.framboisier_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Framboisier Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.framboisier_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.framboisier_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.framboisier_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.51tdkcsamisw9ukycpcur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.framboisier_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Framboisier Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.framboisier_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.framboisier_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.framboisier_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.51tdkcsamisw9ukycpcur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.framboisier_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Framboisier Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.framboisier_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '247.1', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.frysen_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3019,11 +4771,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_state', - 'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state', + 'unique_id': 'tuya.ase6htln9tdni2sijxqbattery_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_battery_state-state] +# name: test_platform_setup_and_discovery[sensor.frysen_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Frysen Battery state', @@ -3036,7 +4788,7 @@ 'state': 'high', }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-entry] +# name: test_platform_setup_and_discovery[sensor.frysen_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3069,11 +4821,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', - 'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value', + 'unique_id': 'tuya.ase6htln9tdni2sijxqhumidity_value', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_humidity-state] +# name: test_platform_setup_and_discovery[sensor.frysen_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'humidity', @@ -3089,7 +4841,7 @@ 'state': '38.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-entry] +# name: test_platform_setup_and_discovery[sensor.frysen_probe_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3125,11 +4877,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_external', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external', + 'unique_id': 'tuya.ase6htln9tdni2sijxqtemp_current_external', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_probe_temperature-state] +# name: test_platform_setup_and_discovery[sensor.frysen_probe_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -3145,7 +4897,7 @@ 'state': '-13.0', }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-entry] +# name: test_platform_setup_and_discovery[sensor.frysen_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3181,11 +4933,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', - 'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current', + 'unique_id': 'tuya.ase6htln9tdni2sijxqtemp_current', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[qxj_is2indt9nlth6esa][sensor.frysen_temperature-state] +# name: test_platform_setup_and_discovery[sensor.frysen_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -3201,7 +4953,513 @@ 'state': '22.2', }) # --- -# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-entry] +# name: test_platform_setup_and_discovery[sensor.garage_contact_sensor_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garage_contact_sensor_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.3uqk1csjqplf3uxqscmbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garage_contact_sensor_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Garage Contact Sensor Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.garage_contact_sensor_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garage_socket_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_socket_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.3d4yosotwk27nqxvzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garage_socket_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Garage Socket Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.garage_socket_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garage_socket_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_socket_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.3d4yosotwk27nqxvzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garage_socket_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Garage Socket Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.garage_socket_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garage_socket_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garage_socket_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.3d4yosotwk27nqxvzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garage_socket_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Garage Socket Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.garage_socket_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '235.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garaz_cerpadlo_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garaz_cerpadlo_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.wc6mumew8inrivi9zccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garaz_cerpadlo_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Garáž čerpadlo Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.garaz_cerpadlo_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garaz_cerpadlo_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garaz_cerpadlo_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.wc6mumew8inrivi9zccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garaz_cerpadlo_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Garáž čerpadlo Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.garaz_cerpadlo_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garaz_cerpadlo_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garaz_cerpadlo_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.wc6mumew8inrivi9zccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garaz_cerpadlo_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Garáž čerpadlo Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.garaz_cerpadlo_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '240.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garden_valve_yard_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.ggimpv4dqzkfsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Garden Valve Yard Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.garden_valve_yard_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_total_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.garden_valve_yard_total_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total watering time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_watering_time', + 'unique_id': 'tuya.ggimpv4dqzkfstime_use', + 'unit_of_measurement': 's', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_total_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Valve Yard Total watering time', + 'state_class': , + 'unit_of_measurement': 's', + }), + 'context': , + 'entity_id': 'sensor.garden_valve_yard_total_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38201.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3234,11 +5492,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas', - 'unique_id': 'tuya.ebb9d0eb5014f98cfboxbzgas_sensor_value', + 'unique_id': 'tuya.cwwk68dyfsh2eqi4jbqrgas_sensor_value', 'unit_of_measurement': 'ppm', }) # --- -# name: test_platform_setup_and_discovery[rqbj_4iqe2hsfyd86kwwc][sensor.gas_sensor_gas-state] +# name: test_platform_setup_and_discovery[sensor.gas_sensor_gas-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Gas sensor Gas', @@ -3253,14 +5511,12 @@ 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_battery-entry] +# name: test_platform_setup_and_discovery[sensor.greenhouse_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), + 'capabilities': None, 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -3268,7 +5524,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.v20_battery', + 'entity_id': 'sensor.greenhouse_battery_state', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3278,35 +5534,85 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Battery', + 'original_name': 'Battery state', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'battery', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtelectricity_left', + 'translation_key': 'battery_state', + 'unique_id': 'tuya.ggwxkj8bwn5y63flgcdswbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.greenhouse_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Greenhouse Battery state', + }), + 'context': , + 'entity_id': 'sensor.greenhouse_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'middle', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.greenhouse_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.greenhouse_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.ggwxkj8bwn5y63flgcdswva_humidity', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_battery-state] +# name: test_platform_setup_and_discovery[sensor.greenhouse_humidity-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'V20 Battery', + 'device_class': 'humidity', + 'friendly_name': 'Greenhouse Humidity', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.v20_battery', + 'entity_id': 'sensor.greenhouse_humidity', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100.0', + 'state': '53.0', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_cleaning_area-entry] +# name: test_platform_setup_and_discovery[sensor.greenhouse_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3321,7 +5627,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.v20_cleaning_area', + 'entity_id': 'sensor.greenhouse_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3330,35 +5636,39 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Cleaning area', + 'original_name': 'Temperature', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cleaning_area', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtclean_area', - 'unit_of_measurement': '㎡', + 'translation_key': 'temperature', + 'unique_id': 'tuya.ggwxkj8bwn5y63flgcdswva_temperature', + 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_cleaning_area-state] +# name: test_platform_setup_and_discovery[sensor.greenhouse_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Cleaning area', + 'device_class': 'temperature', + 'friendly_name': 'Greenhouse Temperature', 'state_class': , - 'unit_of_measurement': '㎡', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.v20_cleaning_area', + 'entity_id': 'sensor.greenhouse_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': '32.2', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_cleaning_time-entry] +# name: test_platform_setup_and_discovery[sensor.hl400_pm2_5-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3373,7 +5683,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.v20_cleaning_time', + 'entity_id': 'sensor.hl400_pm2_5', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3385,32 +5695,32 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Cleaning time', + 'original_name': 'PM2.5', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cleaning_time', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtclean_time', - 'unit_of_measurement': 'min', + 'translation_key': 'pm25', + 'unique_id': 'tuya.zfHZQ7tZUBxAWjACjkpm25', + 'unit_of_measurement': '', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_cleaning_time-state] +# name: test_platform_setup_and_discovery[sensor.hl400_pm2_5-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Cleaning time', + 'friendly_name': 'HL400 PM2.5', 'state_class': , - 'unit_of_measurement': 'min', + 'unit_of_measurement': '', }), 'context': , - 'entity_id': 'sensor.v20_cleaning_time', + 'entity_id': 'sensor.hl400_pm2_5', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': '45.0', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_duster_cloth_lifetime-entry] +# name: test_platform_setup_and_discovery[sensor.hot_water_heat_pump_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3425,7 +5735,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.v20_duster_cloth_lifetime', + 'entity_id': 'sensor.hot_water_heat_pump_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3434,301 +5744,45 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Duster cloth lifetime', + 'original_name': 'Temperature', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'duster_cloth_life', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtduster_cloth', - 'unit_of_measurement': 'min', + 'translation_key': 'temperature', + 'unique_id': 'tuya.ol8xwtcj42eg18bdbrnztemp_current', + 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_duster_cloth_lifetime-state] +# name: test_platform_setup_and_discovery[sensor.hot_water_heat_pump_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Duster cloth lifetime', + 'device_class': 'temperature', + 'friendly_name': 'Hot Water Heat Pump Temperature', 'state_class': , - 'unit_of_measurement': 'min', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.v20_duster_cloth_lifetime', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '9000.0', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_filter_lifetime-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.v20_filter_lifetime', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Filter lifetime', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'filter_life', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtfilter', - 'unit_of_measurement': 'min', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_filter_lifetime-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Filter lifetime', - 'state_class': , - 'unit_of_measurement': 'min', - }), - 'context': , - 'entity_id': 'sensor.v20_filter_lifetime', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8956.0', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_rolling_brush_lifetime-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.v20_rolling_brush_lifetime', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Rolling brush lifetime', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'rolling_brush_life', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtroll_brush', - 'unit_of_measurement': 'min', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_rolling_brush_lifetime-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Rolling brush lifetime', - 'state_class': , - 'unit_of_measurement': 'min', - }), - 'context': , - 'entity_id': 'sensor.v20_rolling_brush_lifetime', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '17948.0', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_side_brush_lifetime-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.v20_side_brush_lifetime', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Side brush lifetime', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'side_brush_life', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtedge_brush', - 'unit_of_measurement': 'min', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_side_brush_lifetime-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Side brush lifetime', - 'state_class': , - 'unit_of_measurement': 'min', - }), - 'context': , - 'entity_id': 'sensor.v20_side_brush_lifetime', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8944.0', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_total_cleaning_area-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.v20_total_cleaning_area', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total cleaning area', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'total_cleaning_area', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmttotal_clean_area', - 'unit_of_measurement': '㎡', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_total_cleaning_area-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Total cleaning area', - 'state_class': , - 'unit_of_measurement': '㎡', - }), - 'context': , - 'entity_id': 'sensor.v20_total_cleaning_area', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '24.0', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_total_cleaning_time-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.v20_total_cleaning_time', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total cleaning time', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'total_cleaning_time', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmttotal_clean_time', - 'unit_of_measurement': 'min', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_total_cleaning_time-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Total cleaning time', - 'state_class': , - 'unit_of_measurement': 'min', - }), - 'context': , - 'entity_id': 'sensor.v20_total_cleaning_time', + 'entity_id': 'sensor.hot_water_heat_pump_temperature', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '42.0', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_total_cleaning_times-entry] +# name: test_platform_setup_and_discovery[sensor.house_water_level_distance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -3737,7 +5791,63 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.v20_total_cleaning_times', + 'entity_id': 'sensor.house_water_level_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'depth', + 'unique_id': 'tuya.snbu4b3vekhywztwqgcwyliquid_depth', + 'unit_of_measurement': 'm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.house_water_level_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'House Water Level Distance', + 'state_class': , + 'unit_of_measurement': 'm', + }), + 'context': , + 'entity_id': 'sensor.house_water_level_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.42', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.house_water_level_liquid_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.house_water_level_liquid_level', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3749,31 +5859,80 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Total cleaning times', + 'original_name': 'Liquid level', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'total_cleaning_times', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmttotal_clean_count', - 'unit_of_measurement': None, + 'translation_key': 'liquid_level', + 'unique_id': 'tuya.snbu4b3vekhywztwqgcwyliquid_level_percent', + 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][sensor.v20_total_cleaning_times-state] +# name: test_platform_setup_and_discovery[sensor.house_water_level_liquid_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Total cleaning times', - 'state_class': , + 'friendly_name': 'House Water Level Liquid level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.v20_total_cleaning_times', + 'entity_id': 'sensor.house_water_level_liquid_level', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.0', + 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][sensor.c9_battery-entry] +# name: test_platform_setup_and_discovery[sensor.house_water_level_liquid_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.house_water_level_liquid_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Liquid state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'liquid_state', + 'unique_id': 'tuya.snbu4b3vekhywztwqgcwyliquid_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.house_water_level_liquid_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'House Water Level Liquid state', + }), + 'context': , + 'entity_id': 'sensor.house_water_level_liquid_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'upper_alarm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_bain_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3788,7 +5947,168 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.c9_battery', + 'entity_id': 'sensor.humy_bain_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.ZgXzZULP6dDp4Atvgcdswva_battery', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_bain_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Humy bain Battery', + 'state_class': , + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'sensor.humy_bain_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_bain_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.humy_bain_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.ZgXzZULP6dDp4Atvgcdswva_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_bain_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Humy bain Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.humy_bain_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '63.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_bain_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.humy_bain_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.ZgXzZULP6dDp4Atvgcdswva_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_bain_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Humy bain Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.humy_bain_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_toilettes_rdc_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.humy_toilettes_rdc_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3806,27 +6126,5313 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfwireless_electricity', + 'unique_id': 'tuya.69dth3rxgcdswbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][sensor.c9_battery-state] +# name: test_platform_setup_and_discovery[sensor.humy_toilettes_rdc_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'C9 Battery', + 'friendly_name': 'Humy toilettes RDC Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.c9_battery', + 'entity_id': 'sensor.humy_toilettes_rdc_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '80.0', + 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][sensor.socket3_current-entry] +# name: test_platform_setup_and_discovery[sensor.humy_toilettes_rdc_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.humy_toilettes_rdc_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.69dth3rxgcdswva_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_toilettes_rdc_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Humy toilettes RDC Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.humy_toilettes_rdc_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '61.8', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_toilettes_rdc_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.humy_toilettes_rdc_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.69dth3rxgcdswva_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.humy_toilettes_rdc_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Humy toilettes RDC Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.humy_toilettes_rdc_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.6', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.hvac_meter_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hvac_meter_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.tcdk0skzcpisexj2zccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.hvac_meter_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'HVAC Meter Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hvac_meter_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.083', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.hvac_meter_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hvac_meter_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.tcdk0skzcpisexj2zccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.hvac_meter_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'HVAC Meter Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.hvac_meter_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.4', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.hvac_meter_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hvac_meter_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.tcdk0skzcpisexj2zccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.hvac_meter_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'HVAC Meter Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.hvac_meter_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '121.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ifs_std002_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.ifs_std002_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.9wlo8cpzprhiclrkgcdswbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ifs_std002_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'IFS-STD002 Battery state', + }), + 'context': , + 'entity_id': 'sensor.ifs_std002_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ifs_std002_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ifs_std002_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.9wlo8cpzprhiclrkgcdswva_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ifs_std002_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'IFS-STD002 Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ifs_std002_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '61.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ifs_std002_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ifs_std002_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.9wlo8cpzprhiclrkgcdswva_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ifs_std002_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'IFS-STD002 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ifs_std002_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.9', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ineox_sp2_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ineox_sp2_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.vx2owjsg86g2ys93zccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ineox_sp2_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Ineox SP2 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ineox_sp2_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.228', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ineox_sp2_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ineox_sp2_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.vx2owjsg86g2ys93zccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ineox_sp2_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Ineox SP2 Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.ineox_sp2_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.1', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ineox_sp2_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ineox_sp2_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.vx2owjsg86g2ys93zccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ineox_sp2_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Ineox SP2 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ineox_sp2_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '232.1', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.inondation_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.inondation_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.qwExlkou9h2USezrjsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.inondation_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Inondation Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.inondation_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ion1000pro_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ion1000pro_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pm25', + 'unique_id': 'tuya.owozxdzgbibizu4sjkpm25', + 'unit_of_measurement': '', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ion1000pro_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO PM2.5', + 'state_class': , + 'unit_of_measurement': '', + }), + 'context': , + 'entity_id': 'sensor.ion1000pro_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.jie_hashui_fa_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jie_hashui_fa_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.jie_hashui_fa_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': '接HA水阀 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.jie_hashui_fa_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.jie_hashui_fa_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.jie_hashui_fa_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.jie_hashui_fa_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '接HA水阀 Battery state', + }), + 'context': , + 'entity_id': 'sensor.jie_hashui_fa_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_air_quality-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.kalado_air_purifier_air_quality', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Air quality', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'air_quality', + 'unique_id': 'tuya.yo2karkjuhzztxsfjkair_quality', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_air_quality-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kalado Air Purifier Air quality', + }), + 'context': , + 'entity_id': 'sensor.kalado_air_purifier_air_quality', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'great', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_filter_utilization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.kalado_air_purifier_filter_utilization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter utilization', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_utilization', + 'unique_id': 'tuya.yo2karkjuhzztxsfjkfilter', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kalado_air_purifier_filter_utilization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kalado Air Purifier Filter utilization', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.kalado_air_purifier_filter_utilization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.keller_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.keller_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.g7af6lrt4miugbstcpcur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.keller_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Keller Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.keller_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.keller_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.keller_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.g7af6lrt4miugbstcpcur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.keller_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Keller Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.keller_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.keller_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.keller_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.g7af6lrt4miugbstcpcur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.keller_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Keller Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.keller_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kippenluik_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.kippenluik_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.AUTwCwqDY9EjlQSocmbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.kippenluik_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kippenluik Battery state', + }), + 'context': , + 'entity_id': 'sensor.kippenluik_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lave_linge_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lave_linge_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.g0edqq0wzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lave_linge_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Lave linge Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lave_linge_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lave_linge_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lave_linge_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.g0edqq0wzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lave_linge_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Lave linge Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.lave_linge_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lave_linge_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lave_linge_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.g0edqq0wzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lave_linge_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Lave linge Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.lave_linge_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '244.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.licht_drucker_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.licht_drucker_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.uvh6oeqrfliovfiwzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.licht_drucker_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Licht drucker Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.licht_drucker_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.licht_drucker_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.licht_drucker_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.uvh6oeqrfliovfiwzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.licht_drucker_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Licht drucker Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.licht_drucker_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.licht_drucker_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.licht_drucker_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.uvh6oeqrfliovfiwzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.licht_drucker_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Licht drucker Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.licht_drucker_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lounge_dark_blind_last_operation_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.lounge_dark_blind_last_operation_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Last operation duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_operation_duration', + 'unique_id': 'tuya.g1efxsqnp33cg8r3lctime_total', + 'unit_of_measurement': 'ms', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.lounge_dark_blind_last_operation_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Lounge Dark Blind Last operation duration', + 'unit_of_measurement': 'ms', + }), + 'context': , + 'entity_id': 'sensor.lounge_dark_blind_last_operation_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25400.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.luminosite_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.luminosite_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.ohefbbk9gcdlbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.luminosite_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Luminosité Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.luminosite_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '91.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.luminosite_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.luminosite_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'illuminance', + 'unique_id': 'tuya.ohefbbk9gcdlbright_value', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.luminosite_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'friendly_name': 'Luminosité Illuminance', + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.luminosite_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.meter_phase_a_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_phase_a_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_current', + 'unique_id': 'tuya.nnqlg0rxryraf8ezbdnzphase_aelectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.meter_phase_a_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Meter Phase A current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.meter_phase_a_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.62', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.meter_phase_a_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_phase_a_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_power', + 'unique_id': 'tuya.nnqlg0rxryraf8ezbdnzphase_apower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.meter_phase_a_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Meter Phase A power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.meter_phase_a_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.185', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.meter_phase_a_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_phase_a_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_voltage', + 'unique_id': 'tuya.nnqlg0rxryraf8ezbdnzphase_avoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.meter_phase_a_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Meter Phase A voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.meter_phase_a_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '233.8', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_current', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_aelectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase A current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.637', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_power', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_apower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase A power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.108', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_voltage', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_avoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_a_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase A voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_a_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '221.1', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_current', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_belectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase B current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.203', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_power', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_bpower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase B power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.41', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_voltage', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_bvoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_b_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase B voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_b_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '218.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_current', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_celectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase C current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.913', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_power', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_cpower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase C power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.092', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_voltage', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldphase_cvoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_phase_c_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Metering_3PN_WiFi_stable Phase C voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_phase_c_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '220.4', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_supply_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_supply_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Supply frequency', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'supply_frequency', + 'unique_id': 'tuya.obb7p55c0us6rdxkqldsupply_frequency', + 'unit_of_measurement': 'Hz', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.metering_3pn_wifi_stable_supply_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'Metering_3PN_WiFi_stable Supply frequency', + 'state_class': , + 'unit_of_measurement': 'Hz', + }), + 'context': , + 'entity_id': 'sensor.metering_3pn_wifi_stable_supply_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.motion_sensor_lidl_zigbee_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.s3zzjdcfripbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.motion_sensor_lidl_zigbee_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Motion sensor lidl zigbee Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.motion_sensor_lidl_zigbee_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.np_downstairs_north_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.np_downstairs_north_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.vayhq2aj3p3z6y2ggcdswbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.np_downstairs_north_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'NP DownStairs North Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.np_downstairs_north_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.np_downstairs_north_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.np_downstairs_north_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.vayhq2aj3p3z6y2ggcdswva_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.np_downstairs_north_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'NP DownStairs North Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.np_downstairs_north_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '47.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.np_downstairs_north_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.np_downstairs_north_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.vayhq2aj3p3z6y2ggcdswva_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.np_downstairs_north_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'NP DownStairs North Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.np_downstairs_north_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.5', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.office_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.office_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.2x473nefusdo7af6zccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.office_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Office Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.253', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.office_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.office_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.2x473nefusdo7af6zccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.office_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Office Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.office_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.9', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.office_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.office_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.2x473nefusdo7af6zccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.office_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Office Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.office_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '239.6', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_a_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_current', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_aelectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'P1 Energia Elettrica Phase A current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_a_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.4', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_a_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_power', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_apower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Energia Elettrica Phase A power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_a_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.314', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_a_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_voltage', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_avoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_a_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'P1 Energia Elettrica Phase A voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_a_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '215.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_b_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_current', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_belectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'P1 Energia Elettrica Phase B current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_b_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_b_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_power', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_bpower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Energia Elettrica Phase B power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_b_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_b_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_voltage', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_bvoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_b_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'P1 Energia Elettrica Phase B voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_b_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_c_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_current', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_celectriccurrent', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'P1 Energia Elettrica Phase C current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_c_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_c_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_power', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_cpower', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Energia Elettrica Phase C power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_c_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_phase_c_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_voltage', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldphase_cvoltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_phase_c_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'P1 Energia Elettrica Phase C voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_phase_c_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_energia_elettrica_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldtotal_forward_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.p1_energia_elettrica_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'P1 Energia Elettrica Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_energia_elettrica_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22799.6', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.patates_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.uew54dymycjwzbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Patates Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.patates_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.patates_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.uew54dymycjwzbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Patates Battery state', + }), + 'context': , + 'entity_id': 'sensor.patates_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.patates_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.uew54dymycjwzhumidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Patates Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.patates_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.patates_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.uew54dymycjwztemp_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.patates_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Patates Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.patates_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pid_relay_2_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pid_relay_2_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.zyutbek7wdm1b4cgzckwhumidity_value', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pid_relay_2_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'pid_relay_2 Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.pid_relay_2_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pid_relay_2_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pid_relay_2_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.zyutbek7wdm1b4cgzckwtemp_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pid_relay_2_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'pid_relay_2 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pid_relay_2_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pir_outside_stairs_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pir_outside_stairs_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.zoytcemodrn39zqwripbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pir_outside_stairs_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'PIR outside stairs Battery state', + }), + 'context': , + 'entity_id': 'sensor.pir_outside_stairs_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'middle', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_filter_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pixi_smart_drinking_fountain_filter_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Filter duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_duration', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcfilter_life', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_filter_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'PIXI Smart Drinking Fountain Filter duration', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.pixi_smart_drinking_fountain_filter_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18965.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_uv_runtime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pixi_smart_drinking_fountain_uv_runtime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'UV runtime', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_runtime', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcuv_runtime', + 'unit_of_measurement': 's', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_uv_runtime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'PIXI Smart Drinking Fountain UV runtime', + 'state_class': , + 'unit_of_measurement': 's', + }), + 'context': , + 'entity_id': 'sensor.pixi_smart_drinking_fountain_uv_runtime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_water_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water level', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_level_state', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcwater_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_water_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'PIXI Smart Drinking Fountain Water level', + }), + 'context': , + 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'level_3', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_water_pump_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_pump_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water pump duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pump_time', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcpump_time', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_water_pump_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'PIXI Smart Drinking Fountain Water pump duration', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_pump_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18965.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_water_usage_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_usage_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water usage duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_time', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcwater_time', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.pixi_smart_drinking_fountain_water_usage_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'PIXI Smart Drinking Fountain Water usage duration', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.pixi_smart_drinking_fountain_water_usage_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.production_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.production_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_power', + 'unique_id': 'tuya.3phkffywh5nnlj5vbdnztotal_powerpower', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.production_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Production Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.production_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2314.6', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.production_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.production_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.3phkffywh5nnlj5vbdnzforward_energy_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.production_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Production Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.production_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1520.21', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.production_total_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.production_total_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total production', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_production', + 'unique_id': 'tuya.3phkffywh5nnlj5vbdnzreverse_energy_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.production_total_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Production Total production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.production_total_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rainwater_tank_level_distance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.rainwater_tank_level_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'depth', + 'unique_id': 'tuya.fbya6s6rhaoyvl8hqgcwyliquid_depth', + 'unit_of_measurement': 'm', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rainwater_tank_level_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Rainwater Tank Level Distance', + 'state_class': , + 'unit_of_measurement': 'm', + }), + 'context': , + 'entity_id': 'sensor.rainwater_tank_level_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.455', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rainwater_tank_level_liquid_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.rainwater_tank_level_liquid_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Liquid level', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'liquid_level', + 'unique_id': 'tuya.fbya6s6rhaoyvl8hqgcwyliquid_level_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rainwater_tank_level_liquid_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Rainwater Tank Level Liquid level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.rainwater_tank_level_liquid_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rainwater_tank_level_liquid_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.rainwater_tank_level_liquid_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Liquid state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'liquid_state', + 'unique_id': 'tuya.fbya6s6rhaoyvl8hqgcwyliquid_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rainwater_tank_level_liquid_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Rainwater Tank Level Liquid state', + }), + 'context': , + 'entity_id': 'sensor.rainwater_tank_level_liquid_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'normal', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.raspy4_home_assistant_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.raspy4_home_assistant_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.zaszonjgzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.raspy4_home_assistant_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Raspy4 - Home Assistant Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.raspy4_home_assistant_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.033', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.raspy4_home_assistant_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.raspy4_home_assistant_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.zaszonjgzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.raspy4_home_assistant_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Raspy4 - Home Assistant Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.raspy4_home_assistant_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.raspy4_home_assistant_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.raspy4_home_assistant_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.zaszonjgzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.raspy4_home_assistant_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Raspy4 - Home Assistant Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.raspy4_home_assistant_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '244.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rat_trap_hedge_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.rat_trap_hedge_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.hkm4px9ohzozxma3ripbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rat_trap_hedge_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'rat trap hedge Battery state', + }), + 'context': , + 'entity_id': 'sensor.rat_trap_hedge_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rauchmelder_alexsandro_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.rauchmelder_alexsandro_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.o71einxvuuktuljcjbwybattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rauchmelder_alexsandro_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Rauchmelder Alexsandro Battery state', + }), + 'context': , + 'entity_id': 'sensor.rauchmelder_alexsandro_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rauchmelder_drucker_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.rauchmelder_drucker_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.t5zosev6h6wmwyrajbwybattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.rauchmelder_drucker_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Rauchmelder Drucker Battery state', + }), + 'context': , + 'entity_id': 'sensor.rauchmelder_drucker_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sapphire_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sapphire_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.hfqeljop3aihnm73zccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sapphire_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Sapphire Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sapphire_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.135', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sapphire_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sapphire_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.hfqeljop3aihnm73zccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sapphire_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Sapphire Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.sapphire_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '313.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sapphire_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sapphire_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.hfqeljop3aihnm73zccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sapphire_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Sapphire Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sapphire_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2357.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_odor_eliminator_pro_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smart_odor_eliminator_pro_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.rl39uwgaqwjwcbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_odor_eliminator_pro_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Smart Odor Eliminator-Pro Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smart_odor_eliminator_pro_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_odor_eliminator_pro_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_odor_eliminator_pro_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Status', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'odor_elimination_status', + 'unique_id': 'tuya.rl39uwgaqwjwcwork_state_e', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_odor_eliminator_pro_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart Odor Eliminator-Pro Status', + }), + 'context': , + 'entity_id': 'sensor.smart_odor_eliminator_pro_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smart_water_timer_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.bl5cuqxnqzkfsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Smart Water Timer Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smart_water_timer_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_total_watering_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smart_water_timer_total_watering_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total watering time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_watering_time', + 'unique_id': 'tuya.bl5cuqxnqzkfstime_use', + 'unit_of_measurement': 's', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_total_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart Water Timer Total watering time', + 'state_class': , + 'unit_of_measurement': 's', + }), + 'context': , + 'entity_id': 'sensor.smart_water_timer_total_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smogo_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smogo_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.swhtzki3qrz5ydchjbocbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smogo_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Smogo Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smogo_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_alarm_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_alarm_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.p8xoxccrjbwybattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_alarm_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Smoke Alarm Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smoke_alarm_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_alarm_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_alarm_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.p8xoxccrjbwybattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_alarm_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke Alarm Battery state', + }), + 'context': , + 'entity_id': 'sensor.smoke_alarm_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_alarm_smoke_amount-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_alarm_smoke_amount', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smoke amount', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'smoke_amount', + 'unique_id': 'tuya.p8xoxccrjbwysmoke_sensor_value', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_alarm_smoke_amount-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke Alarm Smoke amount', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.smoke_alarm_smoke_amount', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_detector_upstairs_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.jfydgffzmhjed9fgjbwybattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_detector_upstairs_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': ' Smoke detector upstairs Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_detector_upstairs_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.jfydgffzmhjed9fgjbwybattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smoke_detector_upstairs_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': ' Smoke detector upstairs Battery state', + }), + 'context': , + 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.socket3_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3865,11 +11471,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current', - 'unique_id': 'tuya.bf0dc19ab84dc3627ep2uncur_current', + 'unique_id': 'tuya.7zogt3pcwhxhu8upqdtcur_current', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][sensor.socket3_current-state] +# name: test_platform_setup_and_discovery[sensor.socket3_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -3885,7 +11491,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][sensor.socket3_power-entry] +# name: test_platform_setup_and_discovery[sensor.socket3_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3921,11 +11527,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', - 'unique_id': 'tuya.bf0dc19ab84dc3627ep2uncur_power', + 'unique_id': 'tuya.7zogt3pcwhxhu8upqdtcur_power', 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][sensor.socket3_power-state] +# name: test_platform_setup_and_discovery[sensor.socket3_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -3941,7 +11547,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][sensor.socket3_voltage-entry] +# name: test_platform_setup_and_discovery[sensor.socket3_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3980,11 +11586,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage', - 'unique_id': 'tuya.bf0dc19ab84dc3627ep2uncur_voltage', + 'unique_id': 'tuya.7zogt3pcwhxhu8upqdtcur_voltage', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][sensor.socket3_voltage-state] +# name: test_platform_setup_and_discovery[sensor.socket3_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -4000,7 +11606,181 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-entry] +# name: test_platform_setup_and_discovery[sensor.socket4_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.socket4_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.4q5c2am8n1bwb6bszccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.socket4_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Socket4 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.socket4_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.socket4_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.socket4_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.4q5c2am8n1bwb6bszccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.socket4_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Socket4 Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.socket4_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.socket4_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.socket4_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.4q5c2am8n1bwb6bszccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.socket4_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Socket4 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.socket4_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.solar_zijpad_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4033,11 +11813,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_percentage', + 'unique_id': 'tuya.couukaypjdnytbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery-state] +# name: test_platform_setup_and_discovery[sensor.solar_zijpad_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -4053,7 +11833,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-entry] +# name: test_platform_setup_and_discovery[sensor.solar_zijpad_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4084,11 +11864,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_state', - 'unique_id': 'tuya.bfdb773e4ae317e3915h2ibattery_state', + 'unique_id': 'tuya.couukaypjdnytbattery_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][sensor.solar_zijpad_battery_state-state] +# name: test_platform_setup_and_discovery[sensor.solar_zijpad_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Solar zijpad Battery state', @@ -4101,7 +11881,382 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-entry] +# name: test_platform_setup_and_discovery[sensor.sous_vide_current_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sous_vide_current_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current_temperature', + 'unique_id': 'tuya.hyda5jsihokacvaqjzmtemp_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sous_vide_current_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Sous Vide Current temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sous_vide_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sous_vide_remaining_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sous_vide_remaining_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remaining time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_time', + 'unique_id': 'tuya.hyda5jsihokacvaqjzmremain_time', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sous_vide_remaining_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sous Vide Remaining time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sous_vide_remaining_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sous_vide_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sous_vide_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Status', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sous_vide_status', + 'unique_id': 'tuya.hyda5jsihokacvaqjzmstatus', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.sous_vide_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sous Vide Status', + }), + 'context': , + 'entity_id': 'sensor.sous_vide_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.spa_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spa_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.gi69tunb0esxcnefzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.spa_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Spa Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.spa_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.404', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.spa_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spa_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.gi69tunb0esxcnefzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.spa_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Spa Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.spa_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1201.8', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.spa_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spa_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.gi69tunb0esxcnefzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.spa_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Spa Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.spa_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '238.1', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.steel_cage_door_battery_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.steel_cage_door_battery_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery state', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state', + 'unique_id': 'tuya.gvxxy4jitzltz5xhscmbattery_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.steel_cage_door_battery_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Steel cage door Battery state', + }), + 'context': , + 'entity_id': 'sensor.steel_cage_door_battery_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.tournesol_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4116,7 +12271,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.wifi_smart_gas_boiler_thermostat_battery', + 'entity_id': 'sensor.tournesol_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4134,27 +12289,547 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.bfb45cb8a9452fba66lexgbattery_percentage', + 'unique_id': 'tuya.codvtvgtjsbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][sensor.wifi_smart_gas_boiler_thermostat_battery-state] +# name: test_platform_setup_and_discovery[sensor.tournesol_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Battery', + 'friendly_name': 'Tournesol Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.wifi_smart_gas_boiler_thermostat_battery', + 'entity_id': 'sensor.tournesol_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '98.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.v20_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.zrrraytdoanz33rldselectricity_left', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'V20 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.v20_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-entry] +# name: test_platform_setup_and_discovery[sensor.v20_cleaning_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_cleaning_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cleaning area', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cleaning_area', + 'unique_id': 'tuya.zrrraytdoanz33rldsclean_area', + 'unit_of_measurement': '㎡', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_cleaning_area-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Cleaning area', + 'state_class': , + 'unit_of_measurement': '㎡', + }), + 'context': , + 'entity_id': 'sensor.v20_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_cleaning_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_cleaning_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cleaning time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cleaning_time', + 'unique_id': 'tuya.zrrraytdoanz33rldsclean_time', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_cleaning_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Cleaning time', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.v20_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_duster_cloth_lifetime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_duster_cloth_lifetime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Duster cloth lifetime', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'duster_cloth_life', + 'unique_id': 'tuya.zrrraytdoanz33rldsduster_cloth', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_duster_cloth_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Duster cloth lifetime', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.v20_duster_cloth_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9000.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_filter_lifetime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_filter_lifetime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter lifetime', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_life', + 'unique_id': 'tuya.zrrraytdoanz33rldsfilter', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_filter_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Filter lifetime', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.v20_filter_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8956.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_rolling_brush_lifetime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_rolling_brush_lifetime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Rolling brush lifetime', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rolling_brush_life', + 'unique_id': 'tuya.zrrraytdoanz33rldsroll_brush', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_rolling_brush_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Rolling brush lifetime', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.v20_rolling_brush_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17948.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_side_brush_lifetime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_side_brush_lifetime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Side brush lifetime', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'side_brush_life', + 'unique_id': 'tuya.zrrraytdoanz33rldsedge_brush', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_side_brush_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Side brush lifetime', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.v20_side_brush_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8944.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_total_cleaning_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_total_cleaning_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total cleaning area', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_cleaning_area', + 'unique_id': 'tuya.zrrraytdoanz33rldstotal_clean_area', + 'unit_of_measurement': '㎡', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_total_cleaning_area-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Total cleaning area', + 'state_class': , + 'unit_of_measurement': '㎡', + }), + 'context': , + 'entity_id': 'sensor.v20_total_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_total_cleaning_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_total_cleaning_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total cleaning time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_cleaning_time', + 'unique_id': 'tuya.zrrraytdoanz33rldstotal_clean_time', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_total_cleaning_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Total cleaning time', + 'state_class': , + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'sensor.v20_total_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_total_cleaning_times-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.v20_total_cleaning_times', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total cleaning times', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_cleaning_times', + 'unique_id': 'tuya.zrrraytdoanz33rldstotal_clean_count', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.v20_total_cleaning_times-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Total cleaning times', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.v20_total_cleaning_times', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.valve_controller_2_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4169,7 +12844,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.np_downstairs_north_battery', + 'entity_id': 'sensor.valve_controller_2_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4187,33 +12862,33 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.bf316b8707b061f044th18battery_percentage', + 'unique_id': 'tuya.kx8dncf1qzkfsbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_battery-state] +# name: test_platform_setup_and_discovery[sensor.valve_controller_2_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'NP DownStairs North Battery', + 'friendly_name': 'Valve Controller 2 Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.np_downstairs_north_battery', + 'entity_id': 'sensor.valve_controller_2_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-entry] +# name: test_platform_setup_and_discovery[sensor.valve_controller_2_total_watering_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -4221,8 +12896,8 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.np_downstairs_north_humidity', + 'entity_category': , + 'entity_id': 'sensor.valve_controller_2_total_watering_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4232,35 +12907,34 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Humidity', + 'original_name': 'Total watering time', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bf316b8707b061f044th18va_humidity', - 'unit_of_measurement': '%', + 'translation_key': 'total_watering_time', + 'unique_id': 'tuya.kx8dncf1qzkfstime_use', + 'unit_of_measurement': 's', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_humidity-state] +# name: test_platform_setup_and_discovery[sensor.valve_controller_2_total_watering_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'NP DownStairs North Humidity', - 'state_class': , - 'unit_of_measurement': '%', + 'friendly_name': 'Valve Controller 2 Total watering time', + 'state_class': , + 'unit_of_measurement': 's', }), 'context': , - 'entity_id': 'sensor.np_downstairs_north_humidity', + 'entity_id': 'sensor.valve_controller_2_total_watering_time', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '47.0', + 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-entry] +# name: test_platform_setup_and_discovery[sensor.varmelampa_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4275,7 +12949,567 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.np_downstairs_north_temperature', + 'entity_id': 'sensor.varmelampa_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.sw1ejdomlmfubapizccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.varmelampa_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Värmelampa Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.varmelampa_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.435', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.varmelampa_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.varmelampa_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.sw1ejdomlmfubapizccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.varmelampa_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Värmelampa Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.varmelampa_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1642.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.varmelampa_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.varmelampa_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.sw1ejdomlmfubapizccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.varmelampa_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Värmelampa Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.varmelampa_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '224.6', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.vividstorm_screen_last_operation_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.vividstorm_screen_last_operation_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Last operation duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_operation_duration', + 'unique_id': 'tuya.4hbnivc4w2rsw966lctime_total', + 'unit_of_measurement': 'ms', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.vividstorm_screen_last_operation_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'VIVIDSTORM SCREEN Last operation duration', + 'unit_of_measurement': 'ms', + }), + 'context': , + 'entity_id': 'sensor.vividstorm_screen_last_operation_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.water_fountain_filter_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_fountain_filter_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_duration', + 'unique_id': 'tuya.q304vac40br8nlkajsywcfilter_life', + 'unit_of_measurement': 'day', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.water_fountain_filter_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Fountain Filter duration', + 'state_class': , + 'unit_of_measurement': 'day', + }), + 'context': , + 'entity_id': 'sensor.water_fountain_filter_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.water_fountain_water_pump_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_fountain_water_pump_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water pump duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'pump_time', + 'unique_id': 'tuya.q304vac40br8nlkajsywcpump_time', + 'unit_of_measurement': 'day', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.water_fountain_water_pump_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Fountain Water pump duration', + 'state_class': , + 'unit_of_measurement': 'day', + }), + 'context': , + 'entity_id': 'sensor.water_fountain_water_pump_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.weihnachtsmann_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weihnachtsmann_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.rwp6kdezm97s2nktzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.weihnachtsmann_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Weihnachtsmann Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.weihnachtsmann_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.weihnachtsmann_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weihnachtsmann_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.rwp6kdezm97s2nktzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.weihnachtsmann_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Weihnachtsmann Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.weihnachtsmann_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.weihnachtsmann_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.weihnachtsmann_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.rwp6kdezm97s2nktzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.weihnachtsmann_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Weihnachtsmann Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.weihnachtsmann_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.qifhbafbqubbp3b6qbnnzpower_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wi-Fi solar grid micro inverter (GT) Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4296,27 +13530,83 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', - 'unique_id': 'tuya.bf316b8707b061f044th18va_temperature', + 'unique_id': 'tuya.qifhbafbqubbp3b6qbnnztemp_current', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[wsdcg_g2y6z3p3ja2qhyav][sensor.np_downstairs_north_temperature-state] +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'NP DownStairs North Temperature', + 'friendly_name': 'Wi-Fi solar grid micro inverter (GT) Temperature', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.np_downstairs_north_temperature', + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '18.5', + 'state': '21.9', }) # --- -# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-entry] +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.qifhbafbqubbp3b6qbnnzreverse_energy_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Wi-Fi solar grid micro inverter (GT) Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.19', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wifi_smart_gas_boiler_thermostat_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4331,7 +13621,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.bathroom_smart_switch_battery', + 'entity_id': 'sensor.wifi_smart_gas_boiler_thermostat_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4349,27 +13639,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.mocked_device_idbattery_percentage', + 'unique_id': 'tuya.j6mn1t4ut5end6ifkwbattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[wxkg_l8yaz4um5b3pwyvf][sensor.bathroom_smart_switch_battery-state] +# name: test_platform_setup_and_discovery[sensor.wifi_smart_gas_boiler_thermostat_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Bathroom Smart Switch Battery', + 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.bathroom_smart_switch_battery', + 'entity_id': 'sensor.wifi_smart_gas_boiler_thermostat_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '100.0', }) # --- -# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery-entry] +# name: test_platform_setup_and_discovery[sensor.wifi_smoke_alarm_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4384,7 +13674,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'entity_id': 'sensor.wifi_smoke_alarm_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4402,27 +13692,80 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', - 'unique_id': 'tuya.8670375210521cf1349cbattery_percentage', + 'unique_id': 'tuya.tvgoe1s3fabebcskjbwybattery_percentage', 'unit_of_measurement': '%', }) # --- -# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery-state] +# name: test_platform_setup_and_discovery[sensor.wifi_smoke_alarm_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': ' Smoke detector upstairs Battery', + 'friendly_name': 'WIFI Smoke alarm Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.smoke_detector_upstairs_battery', + 'entity_id': 'sensor.wifi_smoke_alarm_battery', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '16.0', + 'state': '90.0', }) # --- -# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery_state-entry] +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.urm7i0rtdlabqiqygcdswbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'WiFi Temperature & Humidity Sensor Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_battery_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4435,7 +13778,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_battery_state', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4453,24 +13796,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_state', - 'unique_id': 'tuya.8670375210521cf1349cbattery_state', + 'unique_id': 'tuya.urm7i0rtdlabqiqygcdswbattery_state', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[ywbj_gf9dejhmzffgdyfj][sensor.smoke_detector_upstairs_battery_state-state] +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_battery_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': ' Smoke detector upstairs Battery state', + 'friendly_name': 'WiFi Temperature & Humidity Sensor Battery state', }), 'context': , - 'entity_id': 'sensor.smoke_detector_upstairs_battery_state', + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_battery_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'low', + 'state': 'high', }) # --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][sensor.rainwater_tank_level_distance-entry] +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4485,7 +13828,60 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.rainwater_tank_level_distance', + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'humidity', + 'unique_id': 'tuya.urm7i0rtdlabqiqygcdswva_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'WiFi Temperature & Humidity Sensor Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4495,38 +13891,38 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 2, + 'suggested_display_precision': 1, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Distance', + 'original_name': 'Temperature', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'depth', - 'unique_id': 'tuya.bf3d16d38b17d7034ddxd4liquid_depth', - 'unit_of_measurement': 'm', + 'translation_key': 'temperature', + 'unique_id': 'tuya.urm7i0rtdlabqiqygcdswva_temperature', + 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][sensor.rainwater_tank_level_distance-state] +# name: test_platform_setup_and_discovery[sensor.wifi_temperature_humidity_sensor_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'distance', - 'friendly_name': 'Rainwater Tank Level Distance', + 'device_class': 'temperature', + 'friendly_name': 'WiFi Temperature & Humidity Sensor Temperature', 'state_class': , - 'unit_of_measurement': 'm', + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.rainwater_tank_level_distance', + 'entity_id': 'sensor.wifi_temperature_humidity_sensor_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.455', + 'state': '25.1', }) # --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][sensor.rainwater_tank_level_liquid_level-entry] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4541,263 +13937,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.rainwater_tank_level_liquid_level', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Liquid level', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'liquid_level', - 'unique_id': 'tuya.bf3d16d38b17d7034ddxd4liquid_level_percent', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][sensor.rainwater_tank_level_liquid_level-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Rainwater Tank Level Liquid level', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.rainwater_tank_level_liquid_level', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '36.0', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][sensor.rainwater_tank_level_liquid_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.rainwater_tank_level_liquid_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Liquid state', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'liquid_state', - 'unique_id': 'tuya.bf3d16d38b17d7034ddxd4liquid_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_h8lvyoahr6s6aybf][sensor.rainwater_tank_level_liquid_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Rainwater Tank Level Liquid state', - }), - 'context': , - 'entity_id': 'sensor.rainwater_tank_level_liquid_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'normal', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][sensor.house_water_level_distance-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.house_water_level_distance', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Distance', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'depth', - 'unique_id': 'tuya.mocked_device_idliquid_depth', - 'unit_of_measurement': 'm', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][sensor.house_water_level_distance-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'distance', - 'friendly_name': 'House Water Level Distance', - 'state_class': , - 'unit_of_measurement': 'm', - }), - 'context': , - 'entity_id': 'sensor.house_water_level_distance', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.42', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][sensor.house_water_level_liquid_level-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.house_water_level_liquid_level', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Liquid level', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'liquid_level', - 'unique_id': 'tuya.mocked_device_idliquid_level_percent', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][sensor.house_water_level_liquid_level-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'House Water Level Liquid level', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.house_water_level_liquid_level', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100.0', - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][sensor.house_water_level_liquid_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.house_water_level_liquid_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Liquid state', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'liquid_state', - 'unique_id': 'tuya.mocked_device_idliquid_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[ywcgq_wtzwyhkev3b4ubns][sensor.house_water_level_liquid_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'House Water Level Liquid state', - }), - 'context': , - 'entity_id': 'sensor.house_water_level_liquid_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'upper_alarm', - }) -# --- -# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.meter_phase_a_current', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4818,27 +13958,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'phase_a_current', - 'unique_id': 'tuya.bfe33b4c74661f1f1bgacyphase_aelectriccurrent', + 'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzphase_aelectriccurrent', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_current-state] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', - 'friendly_name': 'Meter Phase A current', + 'friendly_name': 'XOCA-DAC212XC V2-S1 Phase A current', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.meter_phase_a_current', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_current', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5.62', + 'state': '599.552', }) # --- -# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-entry] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4853,7 +13993,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.meter_phase_a_power', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4874,27 +14014,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'phase_a_power', - 'unique_id': 'tuya.bfe33b4c74661f1f1bgacyphase_apower', + 'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzphase_apower', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_power-state] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Meter Phase A power', + 'friendly_name': 'XOCA-DAC212XC V2-S1 Phase A power', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.meter_phase_a_power', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.185', + 'state': '6.912', }) # --- -# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-entry] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4909,7 +14049,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.meter_phase_a_voltage', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_voltage', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4930,27 +14070,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'phase_a_voltage', - 'unique_id': 'tuya.bfe33b4c74661f1f1bgacyphase_avoltage', + 'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzphase_avoltage', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[zndb_ze8faryrxr0glqnn][sensor.meter_phase_a_voltage-state] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_phase_a_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', - 'friendly_name': 'Meter Phase A voltage', + 'friendly_name': 'XOCA-DAC212XC V2-S1 Phase A voltage', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.meter_phase_a_voltage', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_phase_a_voltage', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '233.8', + 'state': '52.7', }) # --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_battery-entry] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_supply_frequency-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4965,161 +14105,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.patates_battery', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Battery', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'battery', - 'unique_id': 'tuya.bf1a0431555359ce06ie0zbattery_percentage', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_battery-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'Patates Battery', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.patates_battery', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '20.0', - }) -# --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_battery_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.patates_battery_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Battery state', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'battery_state', - 'unique_id': 'tuya.bf1a0431555359ce06ie0zbattery_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_battery_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Patates Battery state', - }), - 'context': , - 'entity_id': 'sensor.patates_battery_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'low', - }) -# --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_humidity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.patates_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'humidity', - 'unique_id': 'tuya.bf1a0431555359ce06ie0zhumidity', - 'unit_of_measurement': '%', - }) -# --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_humidity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Patates Humidity', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.patates_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '97.0', - }) -# --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.patates_temperature', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_supply_frequency', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -5129,34 +14115,320 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 1, + 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Temperature', + 'original_name': 'Supply frequency', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'temperature', - 'unique_id': 'tuya.bf1a0431555359ce06ie0ztemp_current', - 'unit_of_measurement': , + 'translation_key': 'supply_frequency', + 'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzsupply_frequency', + 'unit_of_measurement': 'Hz', }) # --- -# name: test_platform_setup_and_discovery[zwjcy_myd45weu][sensor.patates_temperature-state] +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_supply_frequency-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Patates Temperature', + 'device_class': 'frequency', + 'friendly_name': 'XOCA-DAC212XC V2-S1 Supply frequency', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': 'Hz', }), 'context': , - 'entity_id': 'sensor.patates_temperature', + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_supply_frequency', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '22.0', + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzforward_energy_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'XOCA-DAC212XC V2-S1 Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_total_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_total_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total production', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_production', + 'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzreverse_energy_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.xoca_dac212xc_v2_s1_total_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'XOCA-DAC212XC V2-S1 Total production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.xoca_dac212xc_v2_s1_total_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.8', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.fcdadqsiax2gvnt0qldcur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': '一路带计量磁保持通断器 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.198', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.fcdadqsiax2gvnt0qldcur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': '一路带计量磁保持通断器 Power', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '495.3', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.fcdadqsiax2gvnt0qldcur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': '一路带计量磁保持通断器 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '231.4', }) # --- diff --git a/tests/components/tuya/snapshots/test_siren.ambr b/tests/components/tuya/snapshots/test_siren.ambr index 876db171c7b..c907d94dc39 100644 --- a/tests/components/tuya/snapshots/test_siren.ambr +++ b/tests/components/tuya/snapshots/test_siren.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-entry] +# name: test_platform_setup_and_discovery[siren.aqi-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -30,11 +30,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.eb14fd1dd93ca2ea34vpinalarm_switch', + 'unique_id': 'tuya.iks13mcaiyie3rryjb2ocalarm_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[co2bj_yrr3eiyiacm31ski][siren.aqi-state] +# name: test_platform_setup_and_discovery[siren.aqi-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'AQI', @@ -48,7 +48,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][siren.siren_veranda-entry] +# name: test_platform_setup_and_discovery[siren.burocam-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,7 +61,7 @@ 'disabled_by': None, 'domain': 'siren', 'entity_category': None, - 'entity_id': 'siren.siren_veranda', + 'entity_id': 'siren.burocam', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -79,25 +79,25 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bf0984adfeffe10d5a3ofdalarm_switch', + 'unique_id': 'tuya.svjjuwykgijjedurpssiren_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sgbj_ulv4nnue7gqp0rjk][siren.siren_veranda-state] +# name: test_platform_setup_and_discovery[siren.burocam-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Siren veranda ', + 'friendly_name': 'Bürocam', 'supported_features': , }), 'context': , - 'entity_id': 'siren.siren_veranda', + 'entity_id': 'siren.burocam', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][siren.c9-entry] +# name: test_platform_setup_and_discovery[siren.c9-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -128,11 +128,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfsiren_switch', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspssiren_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][siren.c9-state] +# name: test_platform_setup_and_discovery[siren.c9-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9', @@ -146,3 +146,52 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[siren.siren_veranda-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'siren', + 'entity_category': None, + 'entity_id': 'siren.siren_veranda', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.kjr0pqg7eunn4vlujbgsalarm_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[siren.siren_veranda-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren veranda ', + 'supported_features': , + }), + 'context': , + 'entity_id': 'siren.siren_veranda', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index d483d852f1a..97ba2e47e11 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[cl_3r8gc33pnqsxfe1g][switch.lounge_dark_blind_reverse-entry] +# name: test_platform_setup_and_discovery[switch.3dprinter_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,784 +12,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.lounge_dark_blind_reverse', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reverse', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'reverse', - 'unique_id': 'tuya.mocked_device_idcontrol_back', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cl_3r8gc33pnqsxfe1g][switch.lounge_dark_blind_reverse-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Lounge Dark Blind Reverse', - }), - 'context': , - 'entity_id': 'switch.lounge_dark_blind_reverse', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.dehumidifer_child_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:account-lock', - 'original_name': 'Child lock', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'child_lock', - 'unique_id': 'tuya.mock_device_idchild_lock', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_child_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifer Child lock', - 'icon': 'mdi:account-lock', - }), - 'context': , - 'entity_id': 'switch.dehumidifer_child_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.dehumidifer_ionizer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:atom', - 'original_name': 'Ionizer', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'ionizer', - 'unique_id': 'tuya.mock_device_idanion', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_ka2wfrdoogpvgzfi][switch.dehumidifer_ionizer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifer Ionizer', - 'icon': 'mdi:atom', - }), - 'context': , - 'entity_id': 'switch.dehumidifer_ionizer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.dehumidifier_child_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:account-lock', - 'original_name': 'Child lock', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'child_lock', - 'unique_id': 'tuya.bf3fce6af592f12df3gbgqchild_lock', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cs_zibqa9dutqyaxym2][switch.dehumidifier_child_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dehumidifier Child lock', - 'icon': 'mdi:account-lock', - }), - 'context': , - 'entity_id': 'switch.dehumidifier_child_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.smart_odor_eliminator_pro_switch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Switch', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'switch', - 'unique_id': 'tuya.bf6574iutyikgwkxswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwjwq_agwu93lr][switch.smart_odor_eliminator_pro_switch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Smart Odor Eliminator-Pro Switch', - }), - 'context': , - 'entity_id': 'switch.smart_odor_eliminator_pro_switch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_filter_reset', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Filter reset', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'filter_reset', - 'unique_id': 'tuya.23536058083a8dc57d96filter_reset', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_filter_reset-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'PIXI Smart Drinking Fountain Filter reset', - }), - 'context': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_filter_reset', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.pixi_smart_drinking_fountain_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Power', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'power', - 'unique_id': 'tuya.23536058083a8dc57d96switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'PIXI Smart Drinking Fountain Power', - }), - 'context': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_reset_of_water_usage_days', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reset of water usage days', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'reset_of_water_usage_days', - 'unique_id': 'tuya.23536058083a8dc57d96water_reset', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'PIXI Smart Drinking Fountain Reset of water usage days', - }), - 'context': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_reset_of_water_usage_days', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_uv_sterilization', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'UV sterilization', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'uv_sterilization', - 'unique_id': 'tuya.23536058083a8dc57d96uv', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_uv_sterilization-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'PIXI Smart Drinking Fountain UV sterilization', - }), - 'context': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_uv_sterilization', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_water_pump_reset', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Water pump reset', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'water_pump_reset', - 'unique_id': 'tuya.23536058083a8dc57d96pump_reset', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cwysj_z3rpyvznfcch99aa][switch.pixi_smart_drinking_fountain_water_pump_reset-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'PIXI Smart Drinking Fountain Water pump reset', - }), - 'context': , - 'entity_id': 'switch.pixi_smart_drinking_fountain_water_pump_reset', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[cz_0g1fmqh6d5io7lcn][switch.apollo_light_socket_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.apollo_light_socket_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Socket 1', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.01155072c4dd573f92b8switch_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cz_0g1fmqh6d5io7lcn][switch.apollo_light_socket_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': 'Apollo light Socket 1', - }), - 'context': , - 'entity_id': 'switch.apollo_light_socket_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.hvac_meter_socket_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Socket 1', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.eb0c772dabbb19d653ssi5switch_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': 'HVAC Meter Socket 1', - }), - 'context': , - 'entity_id': 'switch.hvac_meter_socket_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.hvac_meter_socket_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Socket 2', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.eb0c772dabbb19d653ssi5switch_2', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cz_2jxesipczks0kdct][switch.hvac_meter_socket_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': 'HVAC Meter Socket 2', - }), - 'context': , - 'entity_id': 'switch.hvac_meter_socket_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cz_cuhokdii7ojyw8k2][switch.buitenverlichting_socket_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.buitenverlichting_socket_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Socket 1', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.53703774d8f15ba9efd3switch_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cz_cuhokdii7ojyw8k2][switch.buitenverlichting_socket_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': 'Buitenverlichting Socket 1', - }), - 'context': , - 'entity_id': 'switch.buitenverlichting_socket_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cz_dntgh2ngvshfxpsz][switch.fakkel_veranda_socket_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.fakkel_veranda_socket_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Socket 1', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.bf7a2cdaf3ce28d2f7uqnhswitch_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cz_dntgh2ngvshfxpsz][switch.fakkel_veranda_socket_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': 'fakkel veranda Socket 1', - }), - 'context': , - 'entity_id': 'switch.fakkel_veranda_socket_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][switch.droger_socket_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.droger_socket_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Socket 1', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.051724052462ab286504switch_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[cz_hj0a5c7ckzzexu8l][switch.droger_socket_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': 'droger Socket 1', - }), - 'context': , - 'entity_id': 'switch.droger_socket_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_child_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.wallwasher_front_child_lock', + 'entity_id': 'switch.3dprinter_child_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -807,24 +30,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', - 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2echild_lock', + 'unique_id': 'tuya.pykascx9yfqrxtbgzcchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_child_lock-state] +# name: test_platform_setup_and_discovery[switch.3dprinter_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'wallwasher front Child lock', + 'friendly_name': '3DPrinter Child lock', }), 'context': , - 'entity_id': 'switch.wallwasher_front_child_lock', + 'entity_id': 'switch.3dprinter_child_lock', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_socket_1-entry] +# name: test_platform_setup_and_discovery[switch.3dprinter_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -837,7 +60,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.wallwasher_front_socket_1', + 'entity_id': 'switch.3dprinter_socket_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -855,73 +78,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.bf4c0c538bfe408aa9gr2eswitch_1', + 'unique_id': 'tuya.pykascx9yfqrxtbgzcswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[cz_t0a4hwsf8anfsadp][switch.wallwasher_front_socket_1-state] +# name: test_platform_setup_and_discovery[switch.3dprinter_socket_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': 'wallwasher front Socket 1', + 'friendly_name': '3DPrinter Socket 1', }), 'context': , - 'entity_id': 'switch.wallwasher_front_socket_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Child lock', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'child_lock', - 'unique_id': 'tuya.mocked_device_idchild_lock', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': '一路带计量磁保持通断器 Child lock', - }), - 'context': , - 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock', + 'entity_id': 'switch.3dprinter_socket_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] +# name: test_platform_setup_and_discovery[switch.4_433_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -934,55 +109,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Switch', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'switch', - 'unique_id': 'tuya.mocked_device_idswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[dlq_0tnvg2xaisqdadcf][switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': '一路带计量磁保持通断器 Switch', - }), - 'context': , - 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.qt_switch_switch_1', + 'entity_id': 'switch.4_433_switch_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1000,25 +127,172 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.0665305284f3ebe9fdc1switch_1', + 'unique_id': 'tuya.xenxir4a0tn0p1qcqdtswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kg_gbm9ata1zrzaez4a][switch.qt_switch_switch_1-state] +# name: test_platform_setup_and_discovery[switch.4_433_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': 'QT-Switch Switch 1', + 'friendly_name': '4-433 Switch 1', }), 'context': , - 'entity_id': 'switch.qt_switch_switch_1', + 'entity_id': 'switch.4_433_switch_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_child_lock-entry] +# name: test_platform_setup_and_discovery[switch.4_433_switch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.4_433_switch_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Switch 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_switch', + 'unique_id': 'tuya.xenxir4a0tn0p1qcqdtswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.4_433_switch_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': '4-433 Switch 2', + }), + 'context': , + 'entity_id': 'switch.4_433_switch_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.4_433_switch_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.4_433_switch_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Switch 3', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_switch', + 'unique_id': 'tuya.xenxir4a0tn0p1qcqdtswitch_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.4_433_switch_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': '4-433 Switch 3', + }), + 'context': , + 'entity_id': 'switch.4_433_switch_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.4_433_switch_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.4_433_switch_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Switch 4', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_switch', + 'unique_id': 'tuya.xenxir4a0tn0p1qcqdtswitch_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.4_433_switch_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': '4-433 Switch 4', + }), + 'context': , + 'entity_id': 'switch.4_433_switch_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.6294ha_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1031,7 +305,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.hl400_child_lock', + 'entity_id': 'switch.6294ha_child_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1049,72 +323,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', - 'unique_id': 'tuya.152027113c6105cce49clock', + 'unique_id': 'tuya.q62sg0p3s52thp6zzcchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_child_lock-state] +# name: test_platform_setup_and_discovery[switch.6294ha_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'HL400 Child lock', + 'friendly_name': '6294HA Child lock', }), 'context': , - 'entity_id': 'switch.hl400_child_lock', + 'entity_id': 'switch.6294ha_child_lock', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_ionizer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.hl400_ionizer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Ionizer', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'ionizer', - 'unique_id': 'tuya.152027113c6105cce49canion', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_ionizer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'HL400 Ionizer', - }), - 'context': , - 'entity_id': 'switch.hl400_ionizer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_power-entry] +# name: test_platform_setup_and_discovery[switch.6294ha_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1127,7 +353,105 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.hl400_power', + 'entity_id': 'switch.6294ha_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.q62sg0p3s52thp6zzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.6294ha_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': '6294HA Socket 1', + }), + 'context': , + 'entity_id': 'switch.6294ha_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.6294ha_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.6294ha_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.q62sg0p3s52thp6zzcswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.6294ha_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': '6294HA Socket 2', + }), + 'context': , + 'entity_id': 'switch.6294ha_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ac_charging_control_box_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.ac_charging_control_box_switch', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1139,30 +463,79 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Power', + 'original_name': 'Switch', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'power', - 'unique_id': 'tuya.152027113c6105cce49cswitch', + 'translation_key': 'switch', + 'unique_id': 'tuya.qyy1auihjyoogvb7zdccqswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_power-state] +# name: test_platform_setup_and_discovery[switch.ac_charging_control_box_switch-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'HL400 Power', + 'friendly_name': 'AC charging control box Switch', }), 'context': , - 'entity_id': 'switch.hl400_power', + 'entity_id': 'switch.ac_charging_control_box_switch', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_uv_sterilization-entry] +# name: test_platform_setup_and_discovery[switch.apollo_light_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.apollo_light_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.ncl7oi5d6hqmf1g0zcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.apollo_light_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Apollo light Socket 1', + }), + 'context': , + 'entity_id': 'switch.apollo_light_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.aubess_cooker_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1175,7 +548,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.hl400_uv_sterilization', + 'entity_id': 'switch.aubess_cooker_child_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1187,30 +560,469 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'UV sterilization', + 'original_name': 'Child lock', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'uv_sterilization', - 'unique_id': 'tuya.152027113c6105cce49cuv', + 'translation_key': 'child_lock', + 'unique_id': 'tuya.cju47ovcbeuapei2zcchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_CAjWAxBUZt7QZHfz][switch.hl400_uv_sterilization-state] +# name: test_platform_setup_and_discovery[switch.aubess_cooker_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'HL400 UV sterilization', + 'friendly_name': 'Aubess Cooker Child lock', }), 'context': , - 'entity_id': 'switch.hl400_uv_sterilization', + 'entity_id': 'switch.aubess_cooker_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.aubess_cooker_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.aubess_cooker_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.cju47ovcbeuapei2zcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.aubess_cooker_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Aubess Cooker Socket 1', + }), + 'context': , + 'entity_id': 'switch.aubess_cooker_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.aubess_washing_machine_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.aubess_washing_machine_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.zjh9xhtm3gibs9kizcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.aubess_washing_machine_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aubess Washing Machine Child lock', + }), + 'context': , + 'entity_id': 'switch.aubess_washing_machine_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.aubess_washing_machine_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.aubess_washing_machine_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.zjh9xhtm3gibs9kizcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.aubess_washing_machine_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Aubess Washing Machine Socket 1', + }), + 'context': , + 'entity_id': 'switch.aubess_washing_machine_socket_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-entry] +# name: test_platform_setup_and_discovery[switch.auvelico_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.auvelico_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.hxbonj4yzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.auvelico_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'AuVeLiCo Socket 1', + }), + 'context': , + 'entity_id': 'switch.auvelico_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.balkonbewasserung_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.balkonbewasserung_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.73ov8i8iedtylkzrqzkfsswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.balkonbewasserung_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'balkonbewässerung Switch', + }), + 'context': , + 'entity_id': 'switch.balkonbewasserung_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.bassin_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bassin_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.kvnsoqyfltmf0bknzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.bassin_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Bassin Socket 1', + }), + 'context': , + 'entity_id': 'switch.bassin_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.bathroom_fan_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bathroom_fan_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.5gfyvvg48bsxbbnjzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.bathroom_fan_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Bathroom Fan Socket 1', + }), + 'context': , + 'entity_id': 'switch.bathroom_fan_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.bathroom_light_switch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bathroom_light_switch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Switch 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_switch', + 'unique_id': 'tuya.gluaktf5gkswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.bathroom_light_switch_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'bathroom light Switch 1', + }), + 'context': , + 'entity_id': 'switch.bathroom_light_switch_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.bathroom_mirror_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.bathroom_mirror_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.eway2kw92ncuecarzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.bathroom_mirror_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Bathroom Mirror Socket 1', + }), + 'context': , + 'entity_id': 'switch.bathroom_mirror_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.bree_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1241,11 +1053,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', - 'unique_id': 'tuya.CENSOREDswitch', + 'unique_id': 'tuya.ppgdpsq1xaxlyzryjkswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[kj_yrzylxax1qspdgpp][switch.bree_power-state] +# name: test_platform_setup_and_discovery[switch.bree_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bree Power', @@ -1258,199 +1070,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.tower_fan_ca_407g_smart_ionizer', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Ionizer', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'ionizer', - 'unique_id': 'tuya.mock_device_idanion', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[ks_j9fa8ahzac8uvlfl][switch.tower_fan_ca_407g_smart_ionizer-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Tower Fan CA-407G Smart Ionizer', - }), - 'context': , - 'entity_id': 'switch.tower_fan_ca_407g_smart_ionizer', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.multifunction_alarm_arm_beep', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Arm beep', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'arm_beep', - 'unique_id': 'tuya.123123aba12312312dazubswitch_alarm_sound', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_arm_beep-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Multifunction alarm Arm beep', - }), - 'context': , - 'entity_id': 'switch.multifunction_alarm_arm_beep', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.multifunction_alarm_siren', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Siren', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'siren', - 'unique_id': 'tuya.123123aba12312312dazubswitch_alarm_light', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[mal_gyitctrjj1kefxp2][switch.multifunction_alarm_siren-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Multifunction alarm Siren', - }), - 'context': , - 'entity_id': 'switch.multifunction_alarm_siren', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][switch.sous_vide_start-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.sous_vide_start', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Start', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'start', - 'unique_id': 'tuya.bff434eca843ffc9afmthvstart', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[mzj_qavcakohisj5adyh][switch.sous_vide_start-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sous Vide Start', - }), - 'context': , - 'entity_id': 'switch.sous_vide_start', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_1-entry] +# name: test_platform_setup_and_discovery[switch.bubbelbad_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1481,11 +1101,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.bf2206da15147500969d6eswitch_1', + 'unique_id': 'tuya.pfhwb1v3i7cifa2tcpswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_1-state] +# name: test_platform_setup_and_discovery[switch.bubbelbad_socket_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1499,7 +1119,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_2-entry] +# name: test_platform_setup_and_discovery[switch.bubbelbad_socket_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1530,11 +1150,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.bf2206da15147500969d6eswitch_2', + 'unique_id': 'tuya.pfhwb1v3i7cifa2tcpswitch_2', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[pc_t2afic7i3v1bwhfp][switch.bubbelbad_socket_2-state] +# name: test_platform_setup_and_discovery[switch.bubbelbad_socket_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -1548,7 +1168,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_1-entry] +# name: test_platform_setup_and_discovery[switch.buitenverlichting_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1561,7 +1181,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.terras_socket_1', + 'entity_id': 'switch.buitenverlichting_socket_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1579,122 +1199,25 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.15727703c4dd5709cd78switch_1', + 'unique_id': 'tuya.2k8wyjo7iidkohuczcswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_1-state] +# name: test_platform_setup_and_discovery[switch.buitenverlichting_socket_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': 'Terras Socket 1', + 'friendly_name': 'Buitenverlichting Socket 1', }), 'context': , - 'entity_id': 'switch.terras_socket_1', + 'entity_id': 'switch.buitenverlichting_socket_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.terras_socket_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Socket 2', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'indexed_socket', - 'unique_id': 'tuya.15727703c4dd5709cd78switch_2', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[pc_trjopo1vdlt9q1tg][switch.terras_socket_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': 'Terras Socket 2', - }), - 'context': , - 'entity_id': 'switch.terras_socket_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.ac_charging_control_box_switch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Switch', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'switch', - 'unique_id': 'tuya.bf83514d9c14b426f0fz5yswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[qccdz_7bvgooyjhiua1yyq][switch.ac_charging_control_box_switch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'AC charging control box Switch', - }), - 'context': , - 'entity_id': 'switch.ac_charging_control_box_switch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][switch.v20_do_not_disturb-entry] +# name: test_platform_setup_and_discovery[switch.burocam_flip-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1707,103 +1230,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.v20_do_not_disturb', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Do not disturb', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'do_not_disturb', - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmtswitch_disturb', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][switch.v20_do_not_disturb-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'V20 Do not disturb', - }), - 'context': , - 'entity_id': 'switch.v20_do_not_disturb', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.sprinkler_cesare_switch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Switch', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'switch', - 'unique_id': 'tuya.bfb9bfc18eeaed2d85yt5mswitch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sfkzq_o6dagifntoafakst][switch.sprinkler_cesare_switch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sprinkler Cesare Switch', - }), - 'context': , - 'entity_id': 'switch.sprinkler_cesare_switch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_flip-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.cam_garage_flip', + 'entity_id': 'switch.burocam_flip', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1821,24 +1248,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flip', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_flip', + 'unique_id': 'tuya.svjjuwykgijjedurpsbasic_flip', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_flip-state] +# name: test_platform_setup_and_discovery[switch.burocam_flip-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Flip', + 'friendly_name': 'Bürocam Flip', }), 'context': , - 'entity_id': 'switch.cam_garage_flip', + 'entity_id': 'switch.burocam_flip', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_motion_alarm-entry] +# name: test_platform_setup_and_discovery[switch.burocam_motion_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1851,7 +1278,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.cam_garage_motion_alarm', + 'entity_id': 'switch.burocam_motion_alarm', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1869,24 +1296,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_alarm', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmmotion_switch', + 'unique_id': 'tuya.svjjuwykgijjedurpsmotion_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_motion_alarm-state] +# name: test_platform_setup_and_discovery[switch.burocam_motion_alarm-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Motion alarm', + 'friendly_name': 'Bürocam Motion alarm', }), 'context': , - 'entity_id': 'switch.cam_garage_motion_alarm', + 'entity_id': 'switch.burocam_motion_alarm', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_sound_detection-entry] +# name: test_platform_setup_and_discovery[switch.burocam_motion_tracking-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1899,7 +1326,103 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.cam_garage_sound_detection', + 'entity_id': 'switch.burocam_motion_tracking', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion tracking', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_tracking', + 'unique_id': 'tuya.svjjuwykgijjedurpsmotion_tracking', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.burocam_motion_tracking-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bürocam Motion tracking', + }), + 'context': , + 'entity_id': 'switch.burocam_motion_tracking', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.burocam_privacy_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.burocam_privacy_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Privacy mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'privacy_mode', + 'unique_id': 'tuya.svjjuwykgijjedurpsbasic_private', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.burocam_privacy_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bürocam Privacy mode', + }), + 'context': , + 'entity_id': 'switch.burocam_privacy_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.burocam_sound_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.burocam_sound_detection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1917,24 +1440,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sound_detection', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmdecibel_switch', + 'unique_id': 'tuya.svjjuwykgijjedurpsdecibel_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_sound_detection-state] +# name: test_platform_setup_and_discovery[switch.burocam_sound_detection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Sound detection', + 'friendly_name': 'Bürocam Sound detection', }), 'context': , - 'entity_id': 'switch.cam_garage_sound_detection', + 'entity_id': 'switch.burocam_sound_detection', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_time_watermark-entry] +# name: test_platform_setup_and_discovery[switch.burocam_time_watermark-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1947,7 +1470,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.cam_garage_time_watermark', + 'entity_id': 'switch.burocam_time_watermark', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1965,264 +1488,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'time_watermark', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmbasic_osd', + 'unique_id': 'tuya.svjjuwykgijjedurpsbasic_osd', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_time_watermark-state] +# name: test_platform_setup_and_discovery[switch.burocam_time_watermark-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Time watermark', + 'friendly_name': 'Bürocam Time watermark', }), 'context': , - 'entity_id': 'switch.cam_garage_time_watermark', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_video_recording-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.cam_garage_video_recording', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Video recording', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'video_recording', - 'unique_id': 'tuya.bf7b8e59f8cd49f425mmfmrecord_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_drezasavompxpcgm][switch.cam_garage_video_recording-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM GARAGE Video recording', - }), - 'context': , - 'entity_id': 'switch.cam_garage_video_recording', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_flip-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.cam_porch_flip', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Flip', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'flip', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_flip', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_flip-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Flip', - }), - 'context': , - 'entity_id': 'switch.cam_porch_flip', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_motion_alarm-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.cam_porch_motion_alarm', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Motion alarm', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'motion_alarm', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9motion_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_motion_alarm-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Motion alarm', - }), - 'context': , - 'entity_id': 'switch.cam_porch_motion_alarm', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_sound_detection-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.cam_porch_sound_detection', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sound detection', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'sound_detection', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9decibel_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_sound_detection-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Sound detection', - }), - 'context': , - 'entity_id': 'switch.cam_porch_sound_detection', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_time_watermark-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.cam_porch_time_watermark', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Time watermark', - 'platform': 'tuya', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'time_watermark', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9basic_osd', - 'unit_of_measurement': None, - }) -# --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_time_watermark-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Time watermark', - }), - 'context': , - 'entity_id': 'switch.cam_porch_time_watermark', + 'entity_id': 'switch.burocam_time_watermark', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_video_recording-entry] +# name: test_platform_setup_and_discovery[switch.burocam_video_recording-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2235,7 +1518,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.cam_porch_video_recording', + 'entity_id': 'switch.burocam_video_recording', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2253,24 +1536,24 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'video_recording', - 'unique_id': 'tuya.bf9d5b7ea61ea4c9a6rom9record_switch', + 'unique_id': 'tuya.svjjuwykgijjedurpsrecord_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_rjKXWRohlvOTyLBu][switch.cam_porch_video_recording-state] +# name: test_platform_setup_and_discovery[switch.burocam_video_recording-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'CAM PORCH Video recording', + 'friendly_name': 'Bürocam Video recording', }), 'context': , - 'entity_id': 'switch.cam_porch_video_recording', + 'entity_id': 'switch.burocam_video_recording', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_flip-entry] +# name: test_platform_setup_and_discovery[switch.c9_flip-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2301,11 +1584,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flip', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfbasic_flip', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsbasic_flip', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_flip-state] +# name: test_platform_setup_and_discovery[switch.c9_flip-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9 Flip', @@ -2318,7 +1601,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_motion_alarm-entry] +# name: test_platform_setup_and_discovery[switch.c9_motion_alarm-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2349,11 +1632,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_alarm', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfmotion_switch', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsmotion_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_motion_alarm-state] +# name: test_platform_setup_and_discovery[switch.c9_motion_alarm-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9 Motion alarm', @@ -2366,7 +1649,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_motion_recording-entry] +# name: test_platform_setup_and_discovery[switch.c9_motion_recording-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2397,11 +1680,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_recording', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfmotion_record', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsmotion_record', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_motion_recording-state] +# name: test_platform_setup_and_discovery[switch.c9_motion_recording-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9 Motion recording', @@ -2414,7 +1697,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_motion_tracking-entry] +# name: test_platform_setup_and_discovery[switch.c9_motion_tracking-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2445,11 +1728,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_tracking', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfmotion_tracking', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsmotion_tracking', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_motion_tracking-state] +# name: test_platform_setup_and_discovery[switch.c9_motion_tracking-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9 Motion tracking', @@ -2462,7 +1745,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_time_watermark-entry] +# name: test_platform_setup_and_discovery[switch.c9_time_watermark-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2493,11 +1776,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'time_watermark', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfbasic_osd', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsbasic_osd', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_time_watermark-state] +# name: test_platform_setup_and_discovery[switch.c9_time_watermark-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9 Time watermark', @@ -2510,7 +1793,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_video_recording-entry] +# name: test_platform_setup_and_discovery[switch.c9_video_recording-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2541,11 +1824,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'video_recording', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfrecord_switch', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsrecord_switch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_video_recording-state] +# name: test_platform_setup_and_discovery[switch.c9_video_recording-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9 Video recording', @@ -2558,7 +1841,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_wide_dynamic_range-entry] +# name: test_platform_setup_and_discovery[switch.c9_wide_dynamic_range-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2589,11 +1872,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wide_dynamic_range', - 'unique_id': 'tuya.bf3f8b448bbc123e29oghfbasic_wdr', + 'unique_id': 'tuya.fjdyw5ld2f5f5ddspsbasic_wdr', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sp_sdd5f5f2dl5wydjf][switch.c9_wide_dynamic_range-state] +# name: test_platform_setup_and_discovery[switch.c9_wide_dynamic_range-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'C9 Wide dynamic range', @@ -2606,7 +1889,583 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_1aegphq4yfd50e6b][switch.jardin_fraises_switch_1-entry] +# name: test_platform_setup_and_discovery[switch.cam_garage_flip-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip', + 'unique_id': 'tuya.mgcpxpmovasazerdpsbasic_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Flip', + }), + 'context': , + 'entity_id': 'switch.cam_garage_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_motion_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_motion_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion alarm', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_alarm', + 'unique_id': 'tuya.mgcpxpmovasazerdpsmotion_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_motion_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Motion alarm', + }), + 'context': , + 'entity_id': 'switch.cam_garage_motion_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_sound_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_sound_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound_detection', + 'unique_id': 'tuya.mgcpxpmovasazerdpsdecibel_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_sound_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Sound detection', + }), + 'context': , + 'entity_id': 'switch.cam_garage_sound_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_time_watermark-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_time_watermark', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time watermark', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_watermark', + 'unique_id': 'tuya.mgcpxpmovasazerdpsbasic_osd', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_time_watermark-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Time watermark', + }), + 'context': , + 'entity_id': 'switch.cam_garage_time_watermark', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_video_recording-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_garage_video_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Video recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'video_recording', + 'unique_id': 'tuya.mgcpxpmovasazerdpsrecord_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_garage_video_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM GARAGE Video recording', + }), + 'context': , + 'entity_id': 'switch.cam_garage_video_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_flip-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsbasic_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Flip', + }), + 'context': , + 'entity_id': 'switch.cam_porch_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_motion_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_motion_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion alarm', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_alarm', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsmotion_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_motion_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Motion alarm', + }), + 'context': , + 'entity_id': 'switch.cam_porch_motion_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_sound_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_sound_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound detection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound_detection', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsdecibel_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_sound_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Sound detection', + }), + 'context': , + 'entity_id': 'switch.cam_porch_sound_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_time_watermark-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_time_watermark', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time watermark', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_watermark', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsbasic_osd', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_time_watermark-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Time watermark', + }), + 'context': , + 'entity_id': 'switch.cam_porch_time_watermark', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_video_recording-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.cam_porch_video_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Video recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'video_recording', + 'unique_id': 'tuya.uBLyTOvlhoRWXKjrpsrecord_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.cam_porch_video_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CAM PORCH Video recording', + }), + 'context': , + 'entity_id': 'switch.cam_porch_video_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ceiling_fan_light_v2_sound-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ceiling_fan_light_v2_sound', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound', + 'unique_id': 'tuya.6wxksqu35c61sce9dsffan_beep', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ceiling_fan_light_v2_sound-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ceiling fan/Light v2 Sound', + }), + 'context': , + 'entity_id': 'switch.ceiling_fan_light_v2_sound', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.clima_cucina_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.clima_cucina_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.x7quooqakwchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.clima_cucina_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Clima cucina Child lock', + }), + 'context': , + 'entity_id': 'switch.clima_cucina_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.consommation_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2619,7 +2478,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.jardin_fraises_switch_1', + 'entity_id': 'switch.consommation_socket_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2631,31 +2490,905 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Switch 1', + 'original_name': 'Socket 1', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.bfa008a4f82a56616c69uzswitch_1', + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.49m7h9lh3t8pq6ftzcswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_1aegphq4yfd50e6b][switch.jardin_fraises_switch_1-state] +# name: test_platform_setup_and_discovery[switch.consommation_socket_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': 'jardin Fraises Switch 1', + 'friendly_name': 'Consommation Socket 1', }), 'context': , - 'entity_id': 'switch.jardin_fraises_switch_1', + 'entity_id': 'switch.consommation_socket_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_9htyiowaf5rtdhrv][switch.framboisiers_switch_1-entry] +# name: test_platform_setup_and_discovery[switch.consommation_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.consommation_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.49m7h9lh3t8pq6ftzcswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.consommation_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Consommation Socket 2', + }), + 'context': , + 'entity_id': 'switch.consommation_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.dehumidifer_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.dehumidifer_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:account-lock', + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.ifzgvpgoodrfw2akscchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.dehumidifer_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifer Child lock', + 'icon': 'mdi:account-lock', + }), + 'context': , + 'entity_id': 'switch.dehumidifer_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.dehumidifer_ionizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.dehumidifer_ionizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:atom', + 'original_name': 'Ionizer', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ionizer', + 'unique_id': 'tuya.ifzgvpgoodrfw2akscanion', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.dehumidifer_ionizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifer Ionizer', + 'icon': 'mdi:atom', + }), + 'context': , + 'entity_id': 'switch.dehumidifer_ionizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.dehumidifier_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.dehumidifier_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:account-lock', + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.2myxayqtud9aqbizscchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.dehumidifier_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Dehumidifier Child lock', + 'icon': 'mdi:account-lock', + }), + 'context': , + 'entity_id': 'switch.dehumidifier_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.droger_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.droger_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.l8uxezzkc7c5a0jhzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.droger_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'droger Socket 1', + }), + 'context': , + 'entity_id': 'switch.droger_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.duan_lu_qi_ha_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.duan_lu_qi_ha_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.duan_lu_qi_ha_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '断路器HA Child lock', + }), + 'context': , + 'entity_id': 'switch.duan_lu_qi_ha_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.duan_lu_qi_ha_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.duan_lu_qi_ha_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.duan_lu_qi_ha_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '断路器HA Switch', + }), + 'context': , + 'entity_id': 'switch.duan_lu_qi_ha_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.eau_chaude_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.eau_chaude_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.a3qtb7pulkcc6jdjqldchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.eau_chaude_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Eau Chaude Child lock', + }), + 'context': , + 'entity_id': 'switch.eau_chaude_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.eau_chaude_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.eau_chaude_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.a3qtb7pulkcc6jdjqldswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.eau_chaude_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Eau Chaude Switch', + }), + 'context': , + 'entity_id': 'switch.eau_chaude_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.edesanya_energy_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.edesanya_energy_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.edesanya_energy_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Edesanya Energy Switch', + }), + 'context': , + 'entity_id': 'switch.edesanya_energy_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_kitchen_socket_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.elivco_kitchen_socket_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_kitchen_socket_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Elivco Kitchen Socket Child lock', + }), + 'context': , + 'entity_id': 'switch.elivco_kitchen_socket_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_kitchen_socket_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.elivco_kitchen_socket_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_kitchen_socket_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Elivco Kitchen Socket Socket 1', + }), + 'context': , + 'entity_id': 'switch.elivco_kitchen_socket_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_tv_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.elivco_tv_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_tv_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Elivco TV Child lock', + }), + 'context': , + 'entity_id': 'switch.elivco_tv_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_tv_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.elivco_tv_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.elivco_tv_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Elivco TV Socket 1', + }), + 'context': , + 'entity_id': 'switch.elivco_tv_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.fakkel_veranda_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.fakkel_veranda_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.zspxfhsvgn2hgtndzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.fakkel_veranda_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'fakkel veranda Socket 1', + }), + 'context': , + 'entity_id': 'switch.fakkel_veranda_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.framboisier_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.framboisier_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.51tdkcsamisw9ukycpchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.framboisier_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Framboisier Child lock', + }), + 'context': , + 'entity_id': 'switch.framboisier_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.framboisier_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.framboisier_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.51tdkcsamisw9ukycpswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.framboisier_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Framboisier Socket 1', + }), + 'context': , + 'entity_id': 'switch.framboisier_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.framboisier_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.framboisier_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.51tdkcsamisw9ukycpswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.framboisier_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Framboisier Socket 2', + }), + 'context': , + 'entity_id': 'switch.framboisier_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.framboisiers_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2686,11 +3419,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.bff35871a2f4430058vs8uswitch_1', + 'unique_id': 'tuya.vrhdtr5fawoiyth9qdtswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_9htyiowaf5rtdhrv][switch.framboisiers_switch_1-state] +# name: test_platform_setup_and_discovery[switch.framboisiers_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -2704,7 +3437,343 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-entry] +# name: test_platform_setup_and_discovery[switch.garage_camera_flip-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garage_camera_flip', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flip', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'flip', + 'unique_id': 'tuya.53fnjncm3jywuaznpsbasic_flip', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_flip-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Flip', + }), + 'context': , + 'entity_id': 'switch.garage_camera_flip', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_motion_alarm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garage_camera_motion_alarm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion alarm', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_alarm', + 'unique_id': 'tuya.53fnjncm3jywuaznpsmotion_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_motion_alarm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Motion alarm', + }), + 'context': , + 'entity_id': 'switch.garage_camera_motion_alarm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_motion_recording-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garage_camera_motion_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_recording', + 'unique_id': 'tuya.53fnjncm3jywuaznpsmotion_record', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_motion_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Motion recording', + }), + 'context': , + 'entity_id': 'switch.garage_camera_motion_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_motion_tracking-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garage_camera_motion_tracking', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion tracking', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'motion_tracking', + 'unique_id': 'tuya.53fnjncm3jywuaznpsmotion_tracking', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_motion_tracking-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Motion tracking', + }), + 'context': , + 'entity_id': 'switch.garage_camera_motion_tracking', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_privacy_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garage_camera_privacy_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Privacy mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'privacy_mode', + 'unique_id': 'tuya.53fnjncm3jywuaznpsbasic_private', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_privacy_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Privacy mode', + }), + 'context': , + 'entity_id': 'switch.garage_camera_privacy_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_time_watermark-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garage_camera_time_watermark', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Time watermark', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_watermark', + 'unique_id': 'tuya.53fnjncm3jywuaznpsbasic_osd', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_time_watermark-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Time watermark', + }), + 'context': , + 'entity_id': 'switch.garage_camera_time_watermark', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_video_recording-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garage_camera_video_recording', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Video recording', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'video_recording', + 'unique_id': 'tuya.53fnjncm3jywuaznpsrecord_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_camera_video_recording-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garage Camera Video recording', + }), + 'context': , + 'entity_id': 'switch.garage_camera_video_recording', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_socket_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2717,7 +3786,876 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.4_433_switch_1', + 'entity_id': 'switch.garage_socket_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.3d4yosotwk27nqxvzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garage_socket_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Garage Socket Socket 1', + }), + 'context': , + 'entity_id': 'switch.garage_socket_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garaz_cerpadlo_socket-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.garaz_cerpadlo_socket', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'tuya.wc6mumew8inrivi9zcswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garaz_cerpadlo_socket-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Garáž čerpadlo Socket', + }), + 'context': , + 'entity_id': 'switch.garaz_cerpadlo_socket', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.garden_valve_yard_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.garden_valve_yard_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.ggimpv4dqzkfsswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garden_valve_yard_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Valve Yard Switch', + }), + 'context': , + 'entity_id': 'switch.garden_valve_yard_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.hl400_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.zfHZQ7tZUBxAWjACjklock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 Child lock', + }), + 'context': , + 'entity_id': 'switch.hl400_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_ionizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.hl400_ionizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ionizer', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ionizer', + 'unique_id': 'tuya.zfHZQ7tZUBxAWjACjkanion', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_ionizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 Ionizer', + }), + 'context': , + 'entity_id': 'switch.hl400_ionizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.hl400_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.zfHZQ7tZUBxAWjACjkswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 Power', + }), + 'context': , + 'entity_id': 'switch.hl400_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_uv_sterilization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.hl400_uv_sterilization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV sterilization', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_sterilization', + 'unique_id': 'tuya.zfHZQ7tZUBxAWjACjkuv', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.hl400_uv_sterilization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'HL400 UV sterilization', + }), + 'context': , + 'entity_id': 'switch.hl400_uv_sterilization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.home_gateway_mute-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.home_gateway_mute', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mute', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mute', + 'unique_id': 'tuya.sdq2flqkq0lblcah2gwmuffling', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.home_gateway_mute-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Home Gateway Mute', + }), + 'context': , + 'entity_id': 'switch.home_gateway_mute', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.hot_water_heat_pump_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.hot_water_heat_pump_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.ol8xwtcj42eg18bdbrnzswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.hot_water_heat_pump_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hot Water Heat Pump Switch', + }), + 'context': , + 'entity_id': 'switch.hot_water_heat_pump_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.hvac_meter_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.hvac_meter_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.tcdk0skzcpisexj2zcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.hvac_meter_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'HVAC Meter Socket 1', + }), + 'context': , + 'entity_id': 'switch.hvac_meter_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.hvac_meter_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.hvac_meter_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.tcdk0skzcpisexj2zcswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.hvac_meter_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'HVAC Meter Socket 2', + }), + 'context': , + 'entity_id': 'switch.hvac_meter_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ineox_sp2_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ineox_sp2_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.vx2owjsg86g2ys93zcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ineox_sp2_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ineox SP2 Child lock', + }), + 'context': , + 'entity_id': 'switch.ineox_sp2_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ineox_sp2_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.ineox_sp2_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.vx2owjsg86g2ys93zcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ineox_sp2_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Ineox SP2 Socket 1', + }), + 'context': , + 'entity_id': 'switch.ineox_sp2_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ion1000pro_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.owozxdzgbibizu4sjklock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO Child lock', + }), + 'context': , + 'entity_id': 'switch.ion1000pro_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_filter_cartridge_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ion1000pro_filter_cartridge_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter cartridge reset', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_cartridge_reset', + 'unique_id': 'tuya.owozxdzgbibizu4sjkfilter_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_filter_cartridge_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO Filter cartridge reset', + }), + 'context': , + 'entity_id': 'switch.ion1000pro_filter_cartridge_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_ionizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ion1000pro_ionizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ionizer', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ionizer', + 'unique_id': 'tuya.owozxdzgbibizu4sjkanion', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_ionizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO Ionizer', + }), + 'context': , + 'entity_id': 'switch.ion1000pro_ionizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.ion1000pro_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.owozxdzgbibizu4sjkswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO Power', + }), + 'context': , + 'entity_id': 'switch.ion1000pro_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_uv_sterilization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ion1000pro_uv_sterilization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV sterilization', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_sterilization', + 'unique_id': 'tuya.owozxdzgbibizu4sjkuv', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ion1000pro_uv_sterilization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ION1000PRO UV sterilization', + }), + 'context': , + 'entity_id': 'switch.ion1000pro_uv_sterilization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.jardin_fraises_switch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.jardin_fraises_switch_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2735,25 +4673,169 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_1', + 'unique_id': 'tuya.b6e05dfy4qhpgea1qdtswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_1-state] +# name: test_platform_setup_and_discovery[switch.jardin_fraises_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': '4-433 Switch 1', + 'friendly_name': 'jardin Fraises Switch 1', }), 'context': , - 'entity_id': 'switch.4_433_switch_1', + 'entity_id': 'switch.jardin_fraises_switch_1', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-entry] +# name: test_platform_setup_and_discovery[switch.kabinet_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.kabinet_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.dn7cjik6kwchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.kabinet_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Кабінет Child lock', + }), + 'context': , + 'entity_id': 'switch.kabinet_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.kabinet_frost_protection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.kabinet_frost_protection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Frost protection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'frost_protection', + 'unique_id': 'tuya.dn7cjik6kwfrost', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.kabinet_frost_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Кабінет Frost protection', + }), + 'context': , + 'entity_id': 'switch.kabinet_frost_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.kalado_air_purifier_filter_cartridge_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.kalado_air_purifier_filter_cartridge_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter cartridge reset', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_cartridge_reset', + 'unique_id': 'tuya.yo2karkjuhzztxsfjkfilter_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.kalado_air_purifier_filter_cartridge_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kalado Air Purifier Filter cartridge reset', + }), + 'context': , + 'entity_id': 'switch.kalado_air_purifier_filter_cartridge_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.kalado_air_purifier_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2766,7 +4848,831 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.4_433_switch_2', + 'entity_id': 'switch.kalado_air_purifier_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.yo2karkjuhzztxsfjkswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.kalado_air_purifier_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kalado Air Purifier Power', + }), + 'context': , + 'entity_id': 'switch.kalado_air_purifier_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.keller_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.g7af6lrt4miugbstcpswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Keller Socket 1', + }), + 'context': , + 'entity_id': 'switch.keller_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.keller_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.g7af6lrt4miugbstcpswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Keller Socket 2', + }), + 'context': , + 'entity_id': 'switch.keller_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_socket_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.keller_socket_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 3', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.g7af6lrt4miugbstcpswitch_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_socket_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Keller Socket 3', + }), + 'context': , + 'entity_id': 'switch.keller_socket_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_usb_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.keller_usb_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'USB 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_usb', + 'unique_id': 'tuya.g7af6lrt4miugbstcpswitch_usb1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.keller_usb_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Keller USB 1', + }), + 'context': , + 'entity_id': 'switch.keller_usb_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.lave_linge_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.lave_linge_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.g0edqq0wzcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.lave_linge_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Lave linge Child lock', + }), + 'context': , + 'entity_id': 'switch.lave_linge_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.lave_linge_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.lave_linge_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.g0edqq0wzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.lave_linge_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Lave linge Socket 1', + }), + 'context': , + 'entity_id': 'switch.lave_linge_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.licht_drucker_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.licht_drucker_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.uvh6oeqrfliovfiwzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.licht_drucker_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Licht drucker Socket 1', + }), + 'context': , + 'entity_id': 'switch.licht_drucker_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.lounge_dark_blind_reverse-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.lounge_dark_blind_reverse', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reverse', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reverse', + 'unique_id': 'tuya.g1efxsqnp33cg8r3lccontrol_back', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.lounge_dark_blind_reverse-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Lounge Dark Blind Reverse', + }), + 'context': , + 'entity_id': 'switch.lounge_dark_blind_reverse', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.mesh_gateway_mute-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mesh_gateway_mute', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mute', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mute', + 'unique_id': 'tuya.ingdwog22gwmuffling', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.mesh_gateway_mute-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mesh-Gateway Mute', + }), + 'context': , + 'entity_id': 'switch.mesh_gateway_mute', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.multifunction_alarm_arm_beep-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.multifunction_alarm_arm_beep', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Arm beep', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'arm_beep', + 'unique_id': 'tuya.2pxfek1jjrtctiyglamswitch_alarm_sound', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.multifunction_alarm_arm_beep-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Multifunction alarm Arm beep', + }), + 'context': , + 'entity_id': 'switch.multifunction_alarm_arm_beep', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.multifunction_alarm_siren-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.multifunction_alarm_siren', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Siren', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'siren', + 'unique_id': 'tuya.2pxfek1jjrtctiyglamswitch_alarm_light', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.multifunction_alarm_siren-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Multifunction alarm Siren', + }), + 'context': , + 'entity_id': 'switch.multifunction_alarm_siren', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.office_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.office_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.2x473nefusdo7af6zcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.office_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Office Child lock', + }), + 'context': , + 'entity_id': 'switch.office_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.office_lights_switch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.office_lights_switch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Switch 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_switch', + 'unique_id': 'tuya.O8QpxJwdme33sqn4gkswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.office_lights_switch_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'office lights Switch 1', + }), + 'context': , + 'entity_id': 'switch.office_lights_switch_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.office_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.office_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.2x473nefusdo7af6zcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.office_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Office Socket 1', + }), + 'context': , + 'entity_id': 'switch.office_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.p1_energia_elettrica_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.p1_energia_elettrica_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.bcyciyhhu1g2gk9rqldswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.p1_energia_elettrica_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'P1 Energia Elettrica Switch', + }), + 'context': , + 'entity_id': 'switch.p1_energia_elettrica_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.pid_relay_2_switch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pid_relay_2_switch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Switch 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_switch', + 'unique_id': 'tuya.zyutbek7wdm1b4cgzckwswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.pid_relay_2_switch_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'pid_relay_2 Switch 1', + }), + 'context': , + 'entity_id': 'switch.pid_relay_2_switch_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.pid_relay_2_switch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.pid_relay_2_switch_2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2784,25 +5690,73 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_2', + 'unique_id': 'tuya.zyutbek7wdm1b4cgzckwswitch_2', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_2-state] +# name: test_platform_setup_and_discovery[switch.pid_relay_2_switch_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': '4-433 Switch 2', + 'friendly_name': 'pid_relay_2 Switch 2', }), 'context': , - 'entity_id': 'switch.4_433_switch_2', + 'entity_id': 'switch.pid_relay_2_switch_2', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-entry] +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_filter_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_filter_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter reset', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_reset', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcfilter_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_filter_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'PIXI Smart Drinking Fountain Filter reset', + }), + 'context': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_filter_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2815,7 +5769,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.4_433_switch_3', + 'entity_id': 'switch.pixi_smart_drinking_fountain_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2825,33 +5779,176 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, - 'original_name': 'Switch 3', + 'original_name': 'Power', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_3', + 'translation_key': 'power', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcswitch', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_3-state] +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'outlet', - 'friendly_name': '4-433 Switch 3', + 'friendly_name': 'PIXI Smart Drinking Fountain Power', }), 'context': , - 'entity_id': 'switch.4_433_switch_3', + 'entity_id': 'switch.pixi_smart_drinking_fountain_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-entry] +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_reset_of_water_usage_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset of water usage days', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_of_water_usage_days', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcwater_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_reset_of_water_usage_days-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'PIXI Smart Drinking Fountain Reset of water usage days', + }), + 'context': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_reset_of_water_usage_days', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_uv_sterilization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_uv_sterilization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'UV sterilization', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'uv_sterilization', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcuv', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_uv_sterilization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'PIXI Smart Drinking Fountain UV sterilization', + }), + 'context': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_uv_sterilization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_water_pump_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_water_pump_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water pump reset', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_pump_reset', + 'unique_id': 'tuya.aa99hccfnzvypr3zjsywcpump_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.pixi_smart_drinking_fountain_water_pump_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'PIXI Smart Drinking Fountain Water pump reset', + }), + 'context': , + 'entity_id': 'switch.pixi_smart_drinking_fountain_water_pump_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.powerplug_5_socket_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2864,7 +5961,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.4_433_switch_4', + 'entity_id': 'switch.powerplug_5_socket_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2876,31 +5973,373 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Switch 4', + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.aje5kxgmhhxdihqizcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.powerplug_5_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Powerplug 5 Socket 1', + }), + 'context': , + 'entity_id': 'switch.powerplug_5_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.pro_breeze_30l_compressor_dehumidifier_ionizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pro_breeze_30l_compressor_dehumidifier_ionizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:atom', + 'original_name': 'Ionizer', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ionizer', + 'unique_id': 'tuya.q8dncqpgin4yympiscanion', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.pro_breeze_30l_compressor_dehumidifier_ionizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pro Breeze 30L Compressor Dehumidifier Ionizer', + 'icon': 'mdi:atom', + }), + 'context': , + 'entity_id': 'switch.pro_breeze_30l_compressor_dehumidifier_ionizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.qt_switch_switch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.qt_switch_switch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Switch 1', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.bf082711d275c0c883vb4pswitch_4', + 'unique_id': 'tuya.a4zeazrz1ata9mbggkswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_cq1p0nt0a4rixnex][switch.4_433_switch_4-state] +# name: test_platform_setup_and_discovery[switch.qt_switch_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': '4-433 Switch 4', + 'friendly_name': 'QT-Switch Switch 1', }), 'context': , - 'entity_id': 'switch.4_433_switch_4', + 'entity_id': 'switch.qt_switch_switch_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_child_lock-entry] +# name: test_platform_setup_and_discovery[switch.raspy4_home_assistant_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.raspy4_home_assistant_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.zaszonjgzcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.raspy4_home_assistant_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Raspy4 - Home Assistant Child lock', + }), + 'context': , + 'entity_id': 'switch.raspy4_home_assistant_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.raspy4_home_assistant_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.raspy4_home_assistant_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.zaszonjgzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.raspy4_home_assistant_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Raspy4 - Home Assistant Socket 1', + }), + 'context': , + 'entity_id': 'switch.raspy4_home_assistant_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.rewireable_plug_6930ha_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.rewireable_plug_6930ha_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.LS6FfVBVU1vzBRBHzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.rewireable_plug_6930ha_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Rewireable Plug 6930HA Socket 1', + }), + 'context': , + 'entity_id': 'switch.rewireable_plug_6930ha_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sapphire_socket-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sapphire_socket', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'tuya.hfqeljop3aihnm73zcswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sapphire_socket-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Sapphire Socket', + }), + 'context': , + 'entity_id': 'switch.sapphire_socket', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.schuur_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.schuur_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.NVjuXIQ6QH9eZLHCzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.schuur_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'schuur Socket 1', + }), + 'context': , + 'entity_id': 'switch.schuur_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2931,11 +6370,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', - 'unique_id': 'tuya.d7ca553b5f406266350pocchild_lock', + 'unique_id': 'tuya.kxxrbv93k2vvkconqdtchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_child_lock-state] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Seating side 6-ch Smart Switch Child lock', @@ -2948,7 +6387,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_1-entry] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2979,11 +6418,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.d7ca553b5f406266350pocswitch_1', + 'unique_id': 'tuya.kxxrbv93k2vvkconqdtswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_1-state] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -2997,7 +6436,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_2-entry] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3028,11 +6467,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.d7ca553b5f406266350pocswitch_2', + 'unique_id': 'tuya.kxxrbv93k2vvkconqdtswitch_2', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_2-state] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -3046,7 +6485,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_3-entry] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3077,11 +6516,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.d7ca553b5f406266350pocswitch_3', + 'unique_id': 'tuya.kxxrbv93k2vvkconqdtswitch_3', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_3-state] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -3095,7 +6534,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_4-entry] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_4-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3126,11 +6565,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.d7ca553b5f406266350pocswitch_4', + 'unique_id': 'tuya.kxxrbv93k2vvkconqdtswitch_4', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_4-state] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -3144,7 +6583,7 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_5-entry] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_5-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3175,11 +6614,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.d7ca553b5f406266350pocswitch_5', + 'unique_id': 'tuya.kxxrbv93k2vvkconqdtswitch_5', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_5-state] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_5-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -3193,7 +6632,7 @@ 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_6-entry] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_6-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3224,11 +6663,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.d7ca553b5f406266350pocswitch_6', + 'unique_id': 'tuya.kxxrbv93k2vvkconqdtswitch_6', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_nockvv2k39vbrxxk][switch.seating_side_6_ch_smart_switch_switch_6-state] +# name: test_platform_setup_and_discovery[switch.seating_side_6_ch_smart_switch_switch_6-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -3242,7 +6681,392 @@ 'state': 'on', }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][switch.socket3_switch_1-entry] +# name: test_platform_setup_and_discovery[switch.security_light_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.security_light_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.bxfkpxjgux2fgwnazcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.security_light_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Security Light Child lock', + }), + 'context': , + 'entity_id': 'switch.security_light_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.security_light_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.security_light_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.bxfkpxjgux2fgwnazcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.security_light_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Security Light Socket 1', + }), + 'context': , + 'entity_id': 'switch.security_light_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_odor_eliminator_pro_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.smart_odor_eliminator_pro_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.rl39uwgaqwjwcswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_odor_eliminator_pro_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart Odor Eliminator-Pro Switch', + }), + 'context': , + 'entity_id': 'switch.smart_odor_eliminator_pro_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_thermostats_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.smart_thermostats_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.sb3zdertrw50bgogkwchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_thermostats_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'smart thermostats Child lock', + }), + 'context': , + 'entity_id': 'switch.smart_thermostats_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_thermostats_frost_protection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.smart_thermostats_frost_protection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Frost protection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'frost_protection', + 'unique_id': 'tuya.sb3zdertrw50bgogkwfrost', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_thermostats_frost_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'smart thermostats Frost protection', + }), + 'context': , + 'entity_id': 'switch.smart_thermostats_frost_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_water_timer_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.smart_water_timer_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.bl5cuqxnqzkfsswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_water_timer_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart Water Timer Switch', + }), + 'context': , + 'entity_id': 'switch.smart_water_timer_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smoke_alarm_mute-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.smoke_alarm_mute', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mute', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mute', + 'unique_id': 'tuya.p8xoxccrjbwymuffling', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smoke_alarm_mute-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke Alarm Mute', + }), + 'context': , + 'entity_id': 'switch.smoke_alarm_mute', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.smoke_detector_upstairs_mute-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.smoke_detector_upstairs_mute', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mute', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mute', + 'unique_id': 'tuya.jfydgffzmhjed9fgjbwymuffling', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smoke_detector_upstairs_mute-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': ' Smoke detector upstairs Mute', + }), + 'context': , + 'entity_id': 'switch.smoke_detector_upstairs_mute', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.socket3_switch_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3273,11 +7097,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indexed_switch', - 'unique_id': 'tuya.bf0dc19ab84dc3627ep2unswitch_1', + 'unique_id': 'tuya.7zogt3pcwhxhu8upqdtswitch_1', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tdq_pu8uhxhwcp3tgoz7][switch.socket3_switch_1-state] +# name: test_platform_setup_and_discovery[switch.socket3_switch_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -3291,7 +7115,153 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-entry] +# name: test_platform_setup_and_discovery[switch.socket4_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.socket4_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.4q5c2am8n1bwb6bszcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.socket4_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Socket4 Child lock', + }), + 'context': , + 'entity_id': 'switch.socket4_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.socket4_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.socket4_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.4q5c2am8n1bwb6bszcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.socket4_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Socket4 Socket 1', + }), + 'context': , + 'entity_id': 'switch.socket4_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.solar_heater_pump_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.solar_heater_pump_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.YQLkAe7nyyAxXHiAzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.solar_heater_pump_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Solar Heater Pump Socket 1', + }), + 'context': , + 'entity_id': 'switch.solar_heater_pump_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.solar_zijpad_energy_saving-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3322,11 +7292,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saving', - 'unique_id': 'tuya.bfdb773e4ae317e3915h2iswitch_save_energy', + 'unique_id': 'tuya.couukaypjdnytswitch_save_energy', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[tyndj_pyakuuoc][switch.solar_zijpad_energy_saving-state] +# name: test_platform_setup_and_discovery[switch.solar_zijpad_energy_saving-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Solar zijpad Energy saving', @@ -3339,7 +7309,7 @@ 'state': 'unavailable', }) # --- -# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-entry] +# name: test_platform_setup_and_discovery[switch.sous_vide_start-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3352,7 +7322,104 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.clima_cucina_child_lock', + 'entity_id': 'switch.sous_vide_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': 'tuya.hyda5jsihokacvaqjzmstart', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sous_vide_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sous Vide Start', + }), + 'context': , + 'entity_id': 'switch.sous_vide_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.spa_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.spa_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.gi69tunb0esxcnefzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.spa_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Spa Socket 1', + }), + 'context': , + 'entity_id': 'switch.spa_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.spot_1_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.spot_1_child_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3370,24 +7437,1196 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', - 'unique_id': 'tuya.bf6fc1645146455a2efrexchild_lock', + 'unique_id': 'tuya.kffnst1epj6vr8xnzcchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_aqoouq7x][switch.clima_cucina_child_lock-state] +# name: test_platform_setup_and_discovery[switch.spot_1_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Clima cucina Child lock', + 'friendly_name': 'Spot 1 Child lock', }), 'context': , - 'entity_id': 'switch.clima_cucina_child_lock', + 'entity_id': 'switch.spot_1_child_lock', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] +# name: test_platform_setup_and_discovery[switch.spot_1_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.spot_1_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.kffnst1epj6vr8xnzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.spot_1_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Spot 1 Socket 1', + }), + 'context': , + 'entity_id': 'switch.spot_1_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.spot_4_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.spot_4_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.LJ9zTFQTfMgsG2Ahzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.spot_4_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Spot 4 Socket 1', + }), + 'context': , + 'entity_id': 'switch.spot_4_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sprinkler_cesare_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sprinkler_cesare_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.tskafaotnfigad6oqzkfsswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sprinkler_cesare_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sprinkler Cesare Switch', + }), + 'context': , + 'entity_id': 'switch.sprinkler_cesare_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.steckdose_2_socket-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.steckdose_2_socket', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'socket', + 'unique_id': 'tuya.HzsAAAKFLPABVi8nzcswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.steckdose_2_socket-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Steckdose 2 Socket', + }), + 'context': , + 'entity_id': 'switch.steckdose_2_socket', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sunbeam_bedding_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:power', + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.fasvixqysw1lxvjprdswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Sunbeam Bedding Power', + 'icon': 'mdi:power', + }), + 'context': , + 'entity_id': 'switch.sunbeam_bedding_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_preheat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sunbeam_bedding_preheat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:radiator', + 'original_name': 'Preheat', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.fasvixqysw1lxvjprdpreheat', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_preheat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Sunbeam Bedding Preheat', + 'icon': 'mdi:radiator', + }), + 'context': , + 'entity_id': 'switch.sunbeam_bedding_preheat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_a_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sunbeam_bedding_side_a_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:alpha-a', + 'original_name': 'Side A Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.fasvixqysw1lxvjprdswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_a_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Sunbeam Bedding Side A Power', + 'icon': 'mdi:alpha-a', + }), + 'context': , + 'entity_id': 'switch.sunbeam_bedding_side_a_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_a_preheat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sunbeam_bedding_side_a_preheat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:radiator', + 'original_name': 'Side A Preheat', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.fasvixqysw1lxvjprdpreheat_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_a_preheat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Sunbeam Bedding Side A Preheat', + 'icon': 'mdi:radiator', + }), + 'context': , + 'entity_id': 'switch.sunbeam_bedding_side_a_preheat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_b_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sunbeam_bedding_side_b_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:alpha-b', + 'original_name': 'Side B Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.fasvixqysw1lxvjprdswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_b_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Sunbeam Bedding Side B Power', + 'icon': 'mdi:alpha-b', + }), + 'context': , + 'entity_id': 'switch.sunbeam_bedding_side_b_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_b_preheat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.sunbeam_bedding_side_b_preheat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:radiator', + 'original_name': 'Side B Preheat', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.fasvixqysw1lxvjprdpreheat_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.sunbeam_bedding_side_b_preheat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Sunbeam Bedding Side B Preheat', + 'icon': 'mdi:radiator', + }), + 'context': , + 'entity_id': 'switch.sunbeam_bedding_side_b_preheat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.term_prizemi_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.term_prizemi_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.jm2fsqtzuhqtbo5ykwchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.term_prizemi_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Term - Prizemi Child lock', + }), + 'context': , + 'entity_id': 'switch.term_prizemi_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.terras_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.terras_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.gt1q9tldv1opojrtcpswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.terras_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Terras Socket 1', + }), + 'context': , + 'entity_id': 'switch.terras_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.terras_socket_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.terras_socket_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.gt1q9tldv1opojrtcpswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.terras_socket_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Terras Socket 2', + }), + 'context': , + 'entity_id': 'switch.terras_socket_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.tower_fan_ca_407g_smart_ionizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.tower_fan_ca_407g_smart_ionizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ionizer', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ionizer', + 'unique_id': 'tuya.lflvu8cazha8af9jskanion', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.tower_fan_ca_407g_smart_ionizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tower Fan CA-407G Smart Ionizer', + }), + 'context': , + 'entity_id': 'switch.tower_fan_ca_407g_smart_ionizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.v20_do_not_disturb-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.v20_do_not_disturb', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Do not disturb', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'do_not_disturb', + 'unique_id': 'tuya.zrrraytdoanz33rldsswitch_disturb', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.v20_do_not_disturb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'V20 Do not disturb', + }), + 'context': , + 'entity_id': 'switch.v20_do_not_disturb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.valve_controller_2_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.valve_controller_2_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.kx8dncf1qzkfsswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.valve_controller_2_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Valve Controller 2 Switch', + }), + 'context': , + 'entity_id': 'switch.valve_controller_2_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.varmelampa_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.varmelampa_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.sw1ejdomlmfubapizcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.varmelampa_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Värmelampa Socket 1', + }), + 'context': , + 'entity_id': 'switch.varmelampa_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.wallwasher_front_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.wallwasher_front_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.pdasfna8fswh4a0tzcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.wallwasher_front_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'wallwasher front Child lock', + }), + 'context': , + 'entity_id': 'switch.wallwasher_front_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.wallwasher_front_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.wallwasher_front_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.pdasfna8fswh4a0tzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.wallwasher_front_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'wallwasher front Socket 1', + }), + 'context': , + 'entity_id': 'switch.wallwasher_front_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.water_fountain_filter_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.water_fountain_filter_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter reset', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_reset', + 'unique_id': 'tuya.q304vac40br8nlkajsywcfilter_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.water_fountain_filter_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Fountain Filter reset', + }), + 'context': , + 'entity_id': 'switch.water_fountain_filter_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.water_fountain_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.water_fountain_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.q304vac40br8nlkajsywcswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.water_fountain_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Fountain Power', + }), + 'context': , + 'entity_id': 'switch.water_fountain_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.water_fountain_water_pump_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.water_fountain_water_pump_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water pump reset', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_pump_reset', + 'unique_id': 'tuya.q304vac40br8nlkajsywcpump_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.water_fountain_water_pump_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Water Fountain Water pump reset', + }), + 'context': , + 'entity_id': 'switch.water_fountain_water_pump_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.weihnachtsmann_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.weihnachtsmann_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.rwp6kdezm97s2nktzcchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.weihnachtsmann_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Weihnachtsmann Child lock', + }), + 'context': , + 'entity_id': 'switch.weihnachtsmann_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.weihnachtsmann_socket_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.weihnachtsmann_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.rwp6kdezm97s2nktzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.weihnachtsmann_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Weihnachtsmann Socket 1', + }), + 'context': , + 'entity_id': 'switch.weihnachtsmann_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[switch.wifi_smart_gas_boiler_thermostat_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3418,11 +8657,11 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', - 'unique_id': 'tuya.bfb45cb8a9452fba66lexgchild_lock', + 'unique_id': 'tuya.j6mn1t4ut5end6ifkwchild_lock', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[wk_fi6dne5tu4t1nm6j][switch.wifi_smart_gas_boiler_thermostat_child_lock-state] +# name: test_platform_setup_and_discovery[switch.wifi_smart_gas_boiler_thermostat_child_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Child lock', @@ -3435,3 +8674,243 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[switch.wifi_smart_gas_boiler_thermostat_frost_protection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.wifi_smart_gas_boiler_thermostat_frost_protection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Frost protection', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'frost_protection', + 'unique_id': 'tuya.j6mn1t4ut5end6ifkwfrost', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.wifi_smart_gas_boiler_thermostat_frost_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'WiFi Smart Gas Boiler Thermostat Frost protection', + }), + 'context': , + 'entity_id': 'switch.wifi_smart_gas_boiler_thermostat_frost_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.wifi_smoke_alarm_mute-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.wifi_smoke_alarm_mute', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mute', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mute', + 'unique_id': 'tuya.tvgoe1s3fabebcskjbwymuffling', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.wifi_smoke_alarm_mute-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'WIFI Smoke alarm Mute', + }), + 'context': , + 'entity_id': 'switch.wifi_smoke_alarm_mute', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.xoca_dac212xc_v2_s1_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.xoca_dac212xc_v2_s1_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.9oh1h1uyalfykgg4bdnzswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.xoca_dac212xc_v2_s1_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'XOCA-DAC212XC V2-S1 Switch', + }), + 'context': , + 'entity_id': 'switch.xoca_dac212xc_v2_s1_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.fcdadqsiax2gvnt0qldchild_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '一路带计量磁保持通断器 Child lock', + }), + 'context': , + 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_platform_setup_and_discovery[switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.fcdadqsiax2gvnt0qldswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '一路带计量磁保持通断器 Switch', + }), + 'context': , + 'entity_id': 'switch.yi_lu_dai_ji_liang_ci_bao_chi_tong_duan_qi_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_vacuum.ambr b/tests/components/tuya/snapshots/test_vacuum.ambr index bc9ecd197d4..301a9ea8261 100644 --- a/tests/components/tuya/snapshots/test_vacuum.ambr +++ b/tests/components/tuya/snapshots/test_vacuum.ambr @@ -1,5 +1,54 @@ # serializer version: 1 -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][vacuum.v20-entry] +# name: test_platform_setup_and_discovery[vacuum.hoover-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'vacuum', + 'entity_category': None, + 'entity_id': 'vacuum.hoover', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.mwsaod7fa3gjyh6ids', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[vacuum.hoover-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hoover', + 'supported_features': , + }), + 'context': , + 'entity_id': 'vacuum.hoover', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_platform_setup_and_discovery[vacuum.v20-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -36,11 +85,11 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': None, - 'unique_id': 'tuya.bfa951ca98fcf64fddqlmt', + 'unique_id': 'tuya.zrrraytdoanz33rlds', 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sd_lr33znaodtyarrrz][vacuum.v20-state] +# name: test_platform_setup_and_discovery[vacuum.v20-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'fan_speed': 'strong', diff --git a/tests/components/tuya/snapshots/test_valve.ambr b/tests/components/tuya/snapshots/test_valve.ambr new file mode 100644 index 00000000000..cb5f78a5610 --- /dev/null +++ b/tests/components/tuya/snapshots/test_valve.ambr @@ -0,0 +1,401 @@ +# serializer version: 1 +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 1', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 2', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 2', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 3', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 3', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 4', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 4', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 5', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_5', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 5', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 6', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_6', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 6', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_7-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_7', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 7', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_7', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_7-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 7', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_7', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'valve', + 'entity_category': None, + 'entity_id': 'valve.jie_hashui_fa_valve_8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve 8', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_valve', + 'unique_id': 'tuya.cijerqyssiwrf7deqzkfsswitch_8', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[valve.jie_hashui_fa_valve_8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': '接HA水阀 Valve 8', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.jie_hashui_fa_valve_8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- diff --git a/tests/components/tuya/test_alarm_control_panel.py b/tests/components/tuya/test_alarm_control_panel.py index 71527bd83eb..53721b1add0 100644 --- a/tests/components/tuya/test_alarm_control_panel.py +++ b/tests/components/tuya/test_alarm_control_panel.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice @@ -13,45 +12,21 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.ALARM_CONTROL_PANEL in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.ALARM_CONTROL_PANEL]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.ALARM_CONTROL_PANEL not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.ALARM_CONTROL_PANEL]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_binary_sensor.py b/tests/components/tuya/test_binary_sensor.py index 85dd644b79c..4da79effde7 100644 --- a/tests/components/tuya/test_binary_sensor.py +++ b/tests/components/tuya/test_binary_sensor.py @@ -13,51 +13,27 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, MockDeviceListener, initialize_entry +from . import MockDeviceListener, initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.BINARY_SENSOR in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.BINARY_SENSOR not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.BINARY_SENSOR]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["cs_zibqa9dutqyaxym2"], diff --git a/tests/components/tuya/test_button.py b/tests/components/tuya/test_button.py index b8c6dda4afa..e9a7b43e103 100644 --- a/tests/components/tuya/test_button.py +++ b/tests/components/tuya/test_button.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice @@ -13,45 +12,21 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.BUTTON in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.BUTTON]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.BUTTON not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.BUTTON]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_camera.py b/tests/components/tuya/test_camera.py index 25bfe57ea0c..94295fe1191 100644 --- a/tests/components/tuya/test_camera.py +++ b/tests/components/tuya/test_camera.py @@ -13,7 +13,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform @@ -28,22 +28,18 @@ def mock_getrandbits(): yield -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.CAMERA in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.CAMERA]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform( hass, @@ -51,23 +47,3 @@ async def test_platform_setup_and_discovery( snapshot, mock_config_entry.entry_id, ) - - -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.CAMERA not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.CAMERA]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_climate.py b/tests/components/tuya/test_climate.py index 01fdf469e27..a0da9359ea3 100644 --- a/tests/components/tuya/test_climate.py +++ b/tests/components/tuya/test_climate.py @@ -9,61 +9,40 @@ from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice from homeassistant.components.climate import ( + ATTR_FAN_MODE, + ATTR_HUMIDITY, + ATTR_TEMPERATURE, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_TEMPERATURE, ) from homeassistant.components.tuya import ManagerCompat -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.CLIMATE in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.CLIMATE]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.CLIMATE not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.CLIMATE]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["kt_5wnlzekkstwcdsvm"], @@ -84,11 +63,11 @@ async def test_set_temperature( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - "entity_id": entity_id, - "temperature": 22.7, + ATTR_ENTITY_ID: entity_id, + ATTR_TEMPERATURE: 22.7, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [{"code": "temp_set", "value": 22}] ) @@ -110,16 +89,16 @@ async def test_fan_mode_windspeed( state = hass.states.get(entity_id) assert state is not None, f"{entity_id} does not exist" - assert state.attributes["fan_mode"] == 1 + assert state.attributes[ATTR_FAN_MODE] == 1 await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, { - "entity_id": entity_id, - "fan_mode": 2, + ATTR_ENTITY_ID: entity_id, + ATTR_FAN_MODE: 2, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [{"code": "windspeed", "value": "2"}] ) @@ -146,14 +125,14 @@ async def test_fan_mode_no_valid_code( state = hass.states.get(entity_id) assert state is not None, f"{entity_id} does not exist" - assert state.attributes.get("fan_mode") is None + assert state.attributes.get(ATTR_FAN_MODE) is None with pytest.raises(ServiceNotSupported): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, { - "entity_id": entity_id, - "fan_mode": 2, + ATTR_ENTITY_ID: entity_id, + ATTR_FAN_MODE: 2, }, blocking=True, ) @@ -180,8 +159,8 @@ async def test_set_humidity_not_supported( CLIMATE_DOMAIN, SERVICE_SET_HUMIDITY, { - "entity_id": entity_id, - "humidity": 50, + ATTR_ENTITY_ID: entity_id, + ATTR_HUMIDITY: 50, }, blocking=True, ) diff --git a/tests/components/tuya/test_cover.py b/tests/components/tuya/test_cover.py index 20d84878a58..7206aaf1cff 100644 --- a/tests/components/tuya/test_cover.py +++ b/tests/components/tuya/test_cover.py @@ -9,6 +9,9 @@ from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN as COVER_DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, @@ -16,55 +19,31 @@ from homeassistant.components.cover import ( SERVICE_SET_COVER_TILT_POSITION, ) from homeassistant.components.tuya import ManagerCompat -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.COVER in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.COVER not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.COVER]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["cl_zah67ekd"], @@ -86,10 +65,10 @@ async def test_open_service( COVER_DOMAIN, SERVICE_OPEN_COVER, { - "entity_id": entity_id, + ATTR_ENTITY_ID: entity_id, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [ @@ -120,10 +99,10 @@ async def test_close_service( COVER_DOMAIN, SERVICE_CLOSE_COVER, { - "entity_id": entity_id, + ATTR_ENTITY_ID: entity_id, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [ @@ -153,11 +132,11 @@ async def test_set_position( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, { - "entity_id": entity_id, - "position": 25, + ATTR_ENTITY_ID: entity_id, + ATTR_POSITION: 25, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [ @@ -197,7 +176,7 @@ async def test_percent_state_on_cover( state = hass.states.get(entity_id) assert state is not None, f"{entity_id} does not exist" - assert state.attributes["current_position"] == percent_state + assert state.attributes[ATTR_CURRENT_POSITION] == percent_state @pytest.mark.parametrize( @@ -221,8 +200,8 @@ async def test_set_tilt_position_not_supported( COVER_DOMAIN, SERVICE_SET_COVER_TILT_POSITION, { - "entity_id": entity_id, - "tilt_position": 50, + ATTR_ENTITY_ID: entity_id, + ATTR_TILT_POSITION: 50, }, blocking=True, ) diff --git a/tests/components/tuya/test_event.py b/tests/components/tuya/test_event.py index 3a332dbe5c7..6e493ae41c0 100644 --- a/tests/components/tuya/test_event.py +++ b/tests/components/tuya/test_event.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice @@ -13,45 +12,21 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.EVENT in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.EVENT]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.EVENT not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.EVENT]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_fan.py b/tests/components/tuya/test_fan.py index f6b9a6956bf..992c989e352 100644 --- a/tests/components/tuya/test_fan.py +++ b/tests/components/tuya/test_fan.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice @@ -13,43 +12,21 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.FAN in v] -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.FAN]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.FAN not in v] -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.FAN]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_humidifier.py b/tests/components/tuya/test_humidifier.py index bd3604b25dd..c38e5521990 100644 --- a/tests/components/tuya/test_humidifier.py +++ b/tests/components/tuya/test_humidifier.py @@ -9,60 +9,38 @@ from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, DOMAIN as HUMIDIFIER_DOMAIN, SERVICE_SET_HUMIDITY, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) from homeassistant.components.tuya import ManagerCompat -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.HUMIDIFIER in v] -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.HUMIDIFIER]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.HUMIDIFIER not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.HUMIDIFIER]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["cs_zibqa9dutqyaxym2"], @@ -82,9 +60,9 @@ async def test_turn_on( await hass.services.async_call( HUMIDIFIER_DOMAIN, SERVICE_TURN_ON, - {"entity_id": entity_id}, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [{"code": "switch", "value": True}] ) @@ -109,9 +87,9 @@ async def test_turn_off( await hass.services.async_call( HUMIDIFIER_DOMAIN, SERVICE_TURN_OFF, - {"entity_id": entity_id}, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [{"code": "switch", "value": False}] ) @@ -137,11 +115,11 @@ async def test_set_humidity( HUMIDIFIER_DOMAIN, SERVICE_SET_HUMIDITY, { - "entity_id": entity_id, - "humidity": 50, + ATTR_ENTITY_ID: entity_id, + ATTR_HUMIDITY: 50, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [{"code": "dehumidify_set_value", "value": 50}] ) @@ -149,7 +127,7 @@ async def test_set_humidity( @pytest.mark.parametrize( "mock_device_code", - ["cs_vmxuxszzjwp5smli"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_on_unsupported( hass: HomeAssistant, @@ -158,6 +136,11 @@ async def test_turn_on_unsupported( mock_device: CustomerDevice, ) -> None: """Test turn on service (not supported by this device).""" + # Remove switch control - but keep other functionality + mock_device.status.pop("switch") + mock_device.function.pop("switch") + mock_device.status_range.pop("switch") + entity_id = "humidifier.dehumidifier" await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) @@ -167,19 +150,19 @@ async def test_turn_on_unsupported( await hass.services.async_call( HUMIDIFIER_DOMAIN, SERVICE_TURN_ON, - {"entity_id": entity_id}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) assert err.value.translation_key == "action_dpcode_not_found" assert err.value.translation_placeholders == { "expected": "['switch', 'switch_spray']", - "available": ("[]"), + "available": ("['child_lock', 'countdown_set', 'dehumidify_set_value']"), } @pytest.mark.parametrize( "mock_device_code", - ["cs_vmxuxszzjwp5smli"], + ["cs_zibqa9dutqyaxym2"], ) async def test_turn_off_unsupported( hass: HomeAssistant, @@ -188,6 +171,11 @@ async def test_turn_off_unsupported( mock_device: CustomerDevice, ) -> None: """Test turn off service (not supported by this device).""" + # Remove switch control - but keep other functionality + mock_device.status.pop("switch") + mock_device.function.pop("switch") + mock_device.status_range.pop("switch") + entity_id = "humidifier.dehumidifier" await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) @@ -197,19 +185,19 @@ async def test_turn_off_unsupported( await hass.services.async_call( HUMIDIFIER_DOMAIN, SERVICE_TURN_OFF, - {"entity_id": entity_id}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) assert err.value.translation_key == "action_dpcode_not_found" assert err.value.translation_placeholders == { "expected": "['switch', 'switch_spray']", - "available": ("[]"), + "available": ("['child_lock', 'countdown_set', 'dehumidify_set_value']"), } @pytest.mark.parametrize( "mock_device_code", - ["cs_vmxuxszzjwp5smli"], + ["cs_zibqa9dutqyaxym2"], ) async def test_set_humidity_unsupported( hass: HomeAssistant, @@ -218,6 +206,11 @@ async def test_set_humidity_unsupported( mock_device: CustomerDevice, ) -> None: """Test set humidity service (not supported by this device).""" + # Remove set humidity control - but keep other functionality + mock_device.status.pop("dehumidify_set_value") + mock_device.function.pop("dehumidify_set_value") + mock_device.status_range.pop("dehumidify_set_value") + entity_id = "humidifier.dehumidifier" await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) @@ -228,13 +221,13 @@ async def test_set_humidity_unsupported( HUMIDIFIER_DOMAIN, SERVICE_SET_HUMIDITY, { - "entity_id": entity_id, - "humidity": 50, + ATTR_ENTITY_ID: entity_id, + ATTR_HUMIDITY: 50, }, blocking=True, ) assert err.value.translation_key == "action_dpcode_not_found" assert err.value.translation_placeholders == { "expected": "['dehumidify_set_value']", - "available": ("[]"), + "available": ("['child_lock', 'countdown_set', 'switch']"), } diff --git a/tests/components/tuya/test_init.py b/tests/components/tuya/test_init.py index ab96f58ecd0..545a5a7f07c 100644 --- a/tests/components/tuya/test_init.py +++ b/tests/components/tuya/test_init.py @@ -2,39 +2,67 @@ from __future__ import annotations -import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice from homeassistant.components.tuya import ManagerCompat +from homeassistant.components.tuya.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import initialize_entry +from . import DEVICE_MOCKS, initialize_entry -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_load_json_object_fixture -@pytest.mark.parametrize("mock_device_code", ["ydkt_jevroj5aguwdbs2e"]) -async def test_unsupported_device( +async def test_device_registry( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: CustomerDevice, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test unsupported device.""" + """Validate device registry snapshots for all devices, including unsupported ones.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) - # Device is registered - assert ( - dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id) - == snapshot - ) - # No entities registered - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id + device_registry_entries = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id ) + + # Ensure the device registry contains same amount as DEVICE_MOCKS + assert len(device_registry_entries) == len(DEVICE_MOCKS) + + for device_registry_entry in device_registry_entries: + assert device_registry_entry == snapshot( + name=list(device_registry_entry.identifiers)[0][1] + ) + + # Ensure model is suffixed with "(unsupported)" when no entities are generated + assert (" (unsupported)" in device_registry_entry.model) == ( + not er.async_entries_for_device( + entity_registry, + device_registry_entry.id, + include_disabled_entities=True, + ) + ) + + +async def test_fixtures_valid(hass: HomeAssistant) -> None: + """Ensure Tuya fixture files are valid.""" + # We want to ensure that the fixture files do not contain + # `home_assistant`, `id`, or `terminal_id` keys. + # These are provided by the Tuya diagnostics and should be removed + # from the fixture. + EXCLUDE_KEYS = ("home_assistant", "id", "terminal_id") + + for device_code in DEVICE_MOCKS: + details = await async_load_json_object_fixture( + hass, f"{device_code}.json", DOMAIN + ) + for key in EXCLUDE_KEYS: + assert key not in details, ( + f"Please remove data[`'{key}']` from {device_code}.json" + ) diff --git a/tests/components/tuya/test_light.py b/tests/components/tuya/test_light.py index e3586613876..e87eb139385 100644 --- a/tests/components/tuya/test_light.py +++ b/tests/components/tuya/test_light.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch import pytest @@ -9,68 +10,93 @@ from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) from homeassistant.components.tuya import ManagerCompat -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.LIGHT in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.LIGHT]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.LIGHT not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.LIGHT]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["dj_mki13ie507rlry4r"], ) +@pytest.mark.parametrize( + ("turn_on_input", "expected_commands"), + [ + ( + { + ATTR_WHITE: True, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "work_mode", "value": "white"}, + {"code": "bright_value_v2", "value": 546}, + ], + ), + ( + { + ATTR_BRIGHTNESS: 150, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "bright_value_v2", "value": 592}, + ], + ), + ( + { + ATTR_WHITE: True, + ATTR_BRIGHTNESS: 150, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "work_mode", "value": "white"}, + {"code": "bright_value_v2", "value": 592}, + ], + ), + ( + { + ATTR_WHITE: 150, + }, + [ + {"code": "switch_led", "value": True}, + {"code": "work_mode", "value": "white"}, + {"code": "bright_value_v2", "value": 592}, + ], + ), + ], +) async def test_turn_on_white( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, mock_device: CustomerDevice, + turn_on_input: dict[str, Any], + expected_commands: list[dict[str, Any]], ) -> None: """Test turn_on service.""" entity_id = "light.garage_light" @@ -82,17 +108,14 @@ async def test_turn_on_white( LIGHT_DOMAIN, SERVICE_TURN_ON, { - "entity_id": entity_id, - "white": 150, + ATTR_ENTITY_ID: entity_id, + **turn_on_input, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, - [ - {"code": "switch_led", "value": True}, - {"code": "work_mode", "value": "white"}, - ], + expected_commands, ) @@ -116,10 +139,10 @@ async def test_turn_off( LIGHT_DOMAIN, SERVICE_TURN_OFF, { - "entity_id": entity_id, + ATTR_ENTITY_ID: entity_id, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [{"code": "switch_led", "value": False}] ) diff --git a/tests/components/tuya/test_number.py b/tests/components/tuya/test_number.py index f28d6414170..89124fdf65f 100644 --- a/tests/components/tuya/test_number.py +++ b/tests/components/tuya/test_number.py @@ -8,55 +8,37 @@ import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) from homeassistant.components.tuya import ManagerCompat -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.NUMBER in v] -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.NUMBER]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.NUMBER not in v] -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.NUMBER]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["mal_gyitctrjj1kefxp2"], @@ -77,11 +59,11 @@ async def test_set_value( NUMBER_DOMAIN, SERVICE_SET_VALUE, { - "entity_id": entity_id, - "value": 18, + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 18, }, + blocking=True, ) - await hass.async_block_till_done() mock_manager.send_commands.assert_called_once_with( mock_device.id, [{"code": "delay_set", "value": 18}] ) @@ -113,8 +95,8 @@ async def test_set_value_no_function( NUMBER_DOMAIN, SERVICE_SET_VALUE, { - "entity_id": entity_id, - "value": 18, + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 18, }, blocking=True, ) diff --git a/tests/components/tuya/test_select.py b/tests/components/tuya/test_select.py index 475fab30b90..c35963528d4 100644 --- a/tests/components/tuya/test_select.py +++ b/tests/components/tuya/test_select.py @@ -9,57 +9,36 @@ from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice from homeassistant.components.select import ( + ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) from homeassistant.components.tuya import ManagerCompat -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SELECT in v] -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.SELECT]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SELECT not in v] -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SELECT]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["cl_zah67ekd"], @@ -80,8 +59,8 @@ async def test_select_option( SELECT_DOMAIN, SERVICE_SELECT_OPTION, { - "entity_id": entity_id, - "option": "forward", + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "forward", }, blocking=True, ) @@ -111,8 +90,8 @@ async def test_select_invalid_option( SELECT_DOMAIN, SERVICE_SELECT_OPTION, { - "entity_id": entity_id, - "option": "hello", + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "hello", }, blocking=True, ) diff --git a/tests/components/tuya/test_sensor.py b/tests/components/tuya/test_sensor.py index d0c6054c135..a5d61ea47a6 100644 --- a/tests/components/tuya/test_sensor.py +++ b/tests/components/tuya/test_sensor.py @@ -13,44 +13,22 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SENSOR in v] -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.SENSOR]) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SENSOR not in v] -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SENSOR]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_siren.py b/tests/components/tuya/test_siren.py index 69ccc14e407..1043c0a3a0f 100644 --- a/tests/components/tuya/test_siren.py +++ b/tests/components/tuya/test_siren.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice @@ -13,43 +12,21 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SIREN in v] -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.SIREN]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SIREN not in v] -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SIREN]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_switch.py b/tests/components/tuya/test_switch.py index 6164a5c7af8..e763fe3bd91 100644 --- a/tests/components/tuya/test_switch.py +++ b/tests/components/tuya/test_switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import patch -import pytest from syrupy.assertion import SnapshotAssertion from tuya_sharing import CustomerDevice @@ -13,43 +12,21 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SWITCH in v] -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.parametrize( - "mock_device_code", [k for k, v in DEVICE_MOCKS.items() if Platform.SWITCH not in v] -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) diff --git a/tests/components/tuya/test_vacuum.py b/tests/components/tuya/test_vacuum.py index 1caf298f3c4..5ee5b965137 100644 --- a/tests/components/tuya/test_vacuum.py +++ b/tests/components/tuya/test_vacuum.py @@ -13,54 +13,30 @@ from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, ) -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import DEVICE_MOCKS, initialize_entry +from . import initialize_entry from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.VACUUM in v], -) @patch("homeassistant.components.tuya.PLATFORMS", [Platform.VACUUM]) async def test_platform_setup_and_discovery( hass: HomeAssistant, mock_manager: ManagerCompat, mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, + mock_devices: list[CustomerDevice], entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test platform setup and discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize( - "mock_device_code", - [k for k, v in DEVICE_MOCKS.items() if Platform.VACUUM not in v], -) -@patch("homeassistant.components.tuya.PLATFORMS", [Platform.VACUUM]) -async def test_platform_setup_no_discovery( - hass: HomeAssistant, - mock_manager: ManagerCompat, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, - entity_registry: er.EntityRegistry, -) -> None: - """Test platform setup without discovery.""" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - assert not er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - - @pytest.mark.parametrize( "mock_device_code", ["sd_lr33znaodtyarrrz"], @@ -82,7 +58,7 @@ async def test_return_home( VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, { - "entity_id": entity_id, + ATTR_ENTITY_ID: entity_id, }, blocking=True, ) diff --git a/tests/components/tuya/test_valve.py b/tests/components/tuya/test_valve.py new file mode 100644 index 00000000000..73ccfba7fc4 --- /dev/null +++ b/tests/components/tuya/test_valve.py @@ -0,0 +1,96 @@ +"""Test Tuya valve platform.""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion +from tuya_sharing import CustomerDevice + +from homeassistant.components.tuya import ManagerCompat +from homeassistant.components.valve import ( + DOMAIN as VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import initialize_entry + +from tests.common import MockConfigEntry, snapshot_platform + + +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.VALVE]) +async def test_platform_setup_and_discovery( + hass: HomeAssistant, + mock_manager: ManagerCompat, + mock_config_entry: MockConfigEntry, + mock_devices: list[CustomerDevice], + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test platform setup and discovery.""" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "mock_device_code", + ["sfkzq_ed7frwissyqrejic"], +) +async def test_open_valve( + hass: HomeAssistant, + mock_manager: ManagerCompat, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, +) -> None: + """Test opening a valve.""" + entity_id = "valve.jie_hashui_fa_valve_1" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + + state = hass.states.get(entity_id) + assert state is not None, f"{entity_id} does not exist" + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + mock_manager.send_commands.assert_called_once_with( + mock_device.id, [{"code": "switch_1", "value": True}] + ) + + +@pytest.mark.parametrize( + "mock_device_code", + ["sfkzq_ed7frwissyqrejic"], +) +async def test_close_valve( + hass: HomeAssistant, + mock_manager: ManagerCompat, + mock_config_entry: MockConfigEntry, + mock_device: CustomerDevice, +) -> None: + """Test closing a valve.""" + entity_id = "valve.jie_hashui_fa_valve_1" + await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) + + state = hass.states.get(entity_id) + assert state is not None, f"{entity_id} does not exist" + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + mock_manager.send_commands.assert_called_once_with( + mock_device.id, [{"code": "switch_1", "value": False}] + ) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index b951d95fbdc..0776feece54 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -5,9 +5,10 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch import pytest -from uiprotect import NotAuthorized, NvrError, ProtectApiClient +from uiprotect import NvrError, ProtectApiClient from uiprotect.api import DEVICE_UPDATE_INTERVAL from uiprotect.data import NVR, Bootstrap, CloudAccount, Light +from uiprotect.exceptions import BadRequest, NotAuthorized from homeassistant.components.unifiprotect.const import ( AUTH_RETRIES, @@ -414,6 +415,28 @@ async def test_setup_handles_api_key_creation_failure( ufp.api.set_api_key.assert_not_called() +@pytest.mark.parametrize("mock_user_can_write_nvr", [True], indirect=True) +async def test_setup_handles_api_key_creation_bad_request( + hass: HomeAssistant, ufp: MockUFPFixture, mock_user_can_write_nvr: Mock +) -> None: + """Test handling of API key creation BadRequest error.""" + # Setup: API key is not set, user has write permissions, but creation fails with BadRequest + ufp.api.is_api_key_set.return_value = False + ufp.api.create_api_key = AsyncMock( + side_effect=BadRequest("Invalid API key creation request") + ) + + # Should fail with auth error due to API key creation failure + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() + + assert ufp.entry.state is ConfigEntryState.SETUP_ERROR + + # Verify API key creation was attempted but set_api_key was not called + ufp.api.create_api_key.assert_called_once_with(name="Home Assistant (test home)") + ufp.api.set_api_key.assert_not_called() + + async def test_setup_with_existing_api_key( hass: HomeAssistant, ufp: MockUFPFixture ) -> None: diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 55138430ca0..7cdaf4ca720 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -127,7 +127,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -157,7 +162,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 381cc1caa47..330f14be507 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -134,7 +134,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -166,7 +171,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 60ff0a1ebde..92fbca483fd 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -3,6 +3,7 @@ from __future__ import annotations from enum import Enum +import logging from types import ModuleType from typing import Any @@ -437,11 +438,13 @@ async def test_vacuum_deprecated_state_does_not_break_state( assert state.state == "cleaning" -@pytest.mark.usefixtures("mock_as_custom_component") -async def test_vacuum_log_deprecated_battery_properties( +@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)]) +async def test_vacuum_log_deprecated_battery_using_properties( hass: HomeAssistant, config_flow_fixture: None, caplog: pytest.LogCaptureFixture, + is_built_in: bool, + log_warnings: int, ) -> None: """Test incorrectly using battery properties logs warning.""" @@ -449,7 +452,7 @@ async def test_vacuum_log_deprecated_battery_properties( """Mocked vacuum entity.""" @property - def activity(self) -> str: + def activity(self) -> VacuumActivity: """Return the state of the entity.""" return VacuumActivity.CLEANING @@ -477,7 +480,7 @@ async def test_vacuum_log_deprecated_battery_properties( async_setup_entry=help_async_setup_entry_init, async_unload_entry=help_async_unload_entry, ), - built_in=False, + built_in=is_built_in, ) setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -486,26 +489,27 @@ async def test_vacuum_log_deprecated_battery_properties( assert state is not None assert ( - "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it" - " to the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" - in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == log_warnings ) + assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it" - " to the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" + "integration 'test' is setting the battery_icon which has been deprecated." in caplog.text - ) + ) != is_built_in + assert ( + "integration 'test' is setting the battery_level which has been deprecated." + in caplog.text + ) != is_built_in -@pytest.mark.usefixtures("mock_as_custom_component") -async def test_vacuum_log_deprecated_battery_properties_using_attr( +@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 3)]) +async def test_vacuum_log_deprecated_battery_using_attr( hass: HomeAssistant, config_flow_fixture: None, caplog: pytest.LogCaptureFixture, + is_built_in: bool, + log_warnings: int, ) -> None: """Test incorrectly using _attr_battery_* attribute does log issue and raise repair.""" @@ -531,7 +535,7 @@ async def test_vacuum_log_deprecated_battery_properties_using_attr( async_setup_entry=help_async_setup_entry_init, async_unload_entry=help_async_unload_entry, ), - built_in=False, + built_in=is_built_in, ) setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -541,47 +545,51 @@ async def test_vacuum_log_deprecated_battery_properties_using_attr( entity.start() assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it to" - " the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" - in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == log_warnings ) + assert ( - "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." - " Integration test should implement a sensor instead with a correct device class and link it to" - " the same device. This will stop working in Home Assistant 2026.8," - " please report it to the author of the 'test' custom integration" + "integration 'test' is setting the battery_level which has been deprecated." in caplog.text - ) + ) != is_built_in + assert ( + "integration 'test' is setting the battery_icon which has been deprecated." + in caplog.text + ) != is_built_in await async_start(hass, entity.entity_id) caplog.clear() + await async_start(hass, entity.entity_id) + # Test we only log once assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - not in caplog.text - ) - assert ( - "Detected that custom integration 'test' is setting the battery_icon which has been deprecated." - not in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == 0 ) -@pytest.mark.usefixtures("mock_as_custom_component") +@pytest.mark.parametrize(("is_built_in", "log_warnings"), [(True, 0), (False, 1)]) async def test_vacuum_log_deprecated_battery_supported_feature( hass: HomeAssistant, config_flow_fixture: None, caplog: pytest.LogCaptureFixture, + is_built_in: bool, + log_warnings: int, ) -> None: """Test incorrectly setting battery supported feature logs warning.""" - entity = MockVacuum( - name="Testing", - entity_id="vacuum.test", - ) + class MockVacuum(StateVacuumEntity): + """Mock vacuum class.""" + + _attr_supported_features = ( + VacuumEntityFeature.STATE | VacuumEntityFeature.BATTERY + ) + _attr_name = "Testing" + + entity = MockVacuum() config_entry = MockConfigEntry(domain="test") config_entry.add_to_hass(hass) @@ -592,7 +600,7 @@ async def test_vacuum_log_deprecated_battery_supported_feature( async_setup_entry=help_async_setup_entry_init, async_unload_entry=help_async_unload_entry, ), - built_in=False, + built_in=is_built_in, ) setup_test_component_platform(hass, DOMAIN, [entity], from_config_entry=True) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -601,13 +609,14 @@ async def test_vacuum_log_deprecated_battery_supported_feature( assert state is not None assert ( - "Detected that custom integration 'test' is setting the battery supported feature" - " which has been deprecated. Integration test should remove this as part of migrating" - " the battery level and icon to a sensor. This will stop working in Home Assistant 2026.8" - ", please report it to the author of the 'test' custom integration" - in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == log_warnings ) + assert ( + "integration 'test' is setting the battery supported feature" in caplog.text + ) != is_built_in + async def test_vacuum_not_log_deprecated_battery_properties_during_init( hass: HomeAssistant, @@ -624,7 +633,7 @@ async def test_vacuum_not_log_deprecated_battery_properties_during_init( self._attr_battery_level = 50 @property - def activity(self) -> str: + def activity(self) -> VacuumActivity: """Return the state of the entity.""" return VacuumActivity.CLEANING @@ -635,6 +644,6 @@ async def test_vacuum_not_log_deprecated_battery_properties_during_init( assert entity.battery_level == 50 assert ( - "Detected that custom integration 'test' is setting the battery_level which has been deprecated." - not in caplog.text + len([record for record in caplog.records if record.levelno >= logging.WARNING]) + == 0 ) diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 143520b68c2..e29255cdc72 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -355,7 +355,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '400s-purifier-pm25', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), ]) # --- @@ -393,7 +393,7 @@ 'device_class': 'pm25', 'friendly_name': 'Air Purifier 400s PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.air_purifier_400s_pm2_5', @@ -539,7 +539,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '600s-purifier-pm25', - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), ]) # --- @@ -577,7 +577,7 @@ 'device_class': 'pm25', 'friendly_name': 'Air Purifier 600s PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.air_purifier_600s_pm2_5', diff --git a/tests/components/volvo/__init__.py b/tests/components/volvo/__init__.py index 875052fcf7e..acd608b8d26 100644 --- a/tests/components/volvo/__init__.py +++ b/tests/components/volvo/__init__.py @@ -20,6 +20,12 @@ _MODEL_SPECIFIC_RESPONSES = { "statistics", "vehicle", ], + "xc60_phev_2020": [ + "energy_capabilities", + "energy_state", + "statistics", + "vehicle", + ], "xc90_petrol_2019": ["commands", "statistics", "vehicle"], } diff --git a/tests/components/volvo/conftest.py b/tests/components/volvo/conftest.py index edd3f39998e..fedd3a6ec3f 100644 --- a/tests/components/volvo/conftest.py +++ b/tests/components/volvo/conftest.py @@ -9,7 +9,7 @@ from volvocarsapi.auth import TOKEN_URL from volvocarsapi.models import ( VolvoCarsAvailableCommand, VolvoCarsLocation, - VolvoCarsValueField, + VolvoCarsValueStatusField, VolvoCarsVehicle, ) @@ -98,7 +98,7 @@ async def mock_api(hass: HomeAssistant, full_model: str) -> AsyncGenerator[Async hass, "energy_state", full_model ) energy_state = { - key: VolvoCarsValueField.from_dict(value) + key: VolvoCarsValueStatusField.from_dict(value) for key, value in energy_state_data.items() } engine_status = await async_load_fixture_as_value_field( diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json index 968c759ab27..f3aff11585d 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { @@ -25,7 +25,7 @@ "isSupported": true }, "chargingCurrentLimit": { - "isSupported": true + "isSupported": false }, "chargingPower": { "isSupported": true diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_state.json b/tests/components/volvo/fixtures/ex30_2024/energy_state.json index fe42dba568a..5973100d4ea 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_state.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_state.json @@ -1,53 +1,52 @@ { "batteryChargeLevel": { "status": "OK", - "value": 38, + "value": 90.0, "unit": "percentage", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "electricRange": { "status": "OK", - "value": 90, + "value": 327, "unit": "km", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "chargerConnectionStatus": { "status": "OK", - "value": "DISCONNECTED", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "CONNECTED", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingStatus": { "status": "OK", - "value": "IDLE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "DONE", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingType": { "status": "OK", - "value": "NONE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "AC", + "updatedAt": "2025-08-07T14:30:32Z" }, "chargerPowerStatus": { "status": "OK", - "value": "NO_POWER_AVAILABLE", - "updatedAt": "2025-07-02T08:51:23Z" + "value": "FAULT", + "updatedAt": "2025-08-07T14:30:32Z" }, "estimatedChargingTimeToTargetBatteryChargeLevel": { "status": "OK", - "value": 0, + "value": 2, "unit": "minutes", - "updatedAt": "2025-07-02T08:51:23Z" + "updatedAt": "2025-08-07T14:30:32Z" }, "chargingCurrentLimit": { - "status": "OK", - "value": 32, - "unit": "ampere", - "updatedAt": "2024-03-05T08:38:44Z" + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" }, "targetBatteryChargeLevel": { "status": "OK", "value": 90, "unit": "percentage", - "updatedAt": "2024-09-22T09:40:12Z" + "updatedAt": "2025-08-07T14:49:50Z" }, "chargingPower": { "status": "ERROR", diff --git a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json index 968c759ab27..3523d51e071 100644 --- a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { diff --git a/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json b/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json index 16208571c47..bac596857b0 100644 --- a/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json +++ b/tests/components/volvo/fixtures/xc40_electric_2024/energy_state.json @@ -7,8 +7,8 @@ }, "electricRange": { "status": "OK", - "value": 220, - "unit": "km", + "value": 150, + "unit": "mi", "updatedAt": "2025-07-02T08:51:23Z" }, "chargerConnectionStatus": { diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json new file mode 100644 index 00000000000..331795f545b --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json @@ -0,0 +1,33 @@ +{ + "isSupported": true, + "batteryChargeLevel": { + "isSupported": false + }, + "electricRange": { + "isSupported": false + }, + "chargerConnectionStatus": { + "isSupported": true + }, + "chargingStatus": { + "isSupported": true + }, + "chargingType": { + "isSupported": false + }, + "chargerPowerStatus": { + "isSupported": false + }, + "estimatedChargingTimeToTargetBatteryChargeLevel": { + "isSupported": false + }, + "targetBatteryChargeLevel": { + "isSupported": true + }, + "chargingCurrentLimit": { + "isSupported": false + }, + "chargingPower": { + "isSupported": false + } +} diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json new file mode 100644 index 00000000000..e198bfc8330 --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json @@ -0,0 +1,53 @@ +{ + "batteryChargeLevel": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "electricRange": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "chargerConnectionStatus": { + "status": "OK", + "value": "DISCONNECTED", + "updatedAt": "2025-08-07T20:29:18Z" + }, + "chargingStatus": { + "status": "OK", + "value": "IDLE", + "updatedAt": "2025-08-07T20:29:18Z" + }, + "chargingType": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "chargerPowerStatus": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "estimatedChargingTimeToTargetBatteryChargeLevel": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "chargingCurrentLimit": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + }, + "targetBatteryChargeLevel": { + "status": "OK", + "value": 80, + "unit": "percentage", + "updatedAt": "2024-09-22T09:40:12Z" + }, + "chargingPower": { + "status": "ERROR", + "code": "NOT_SUPPORTED", + "message": "Resource is not supported for this vehicle" + } +} diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/statistics.json b/tests/components/volvo/fixtures/xc60_phev_2020/statistics.json new file mode 100644 index 00000000000..91384f2d13e --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/statistics.json @@ -0,0 +1,32 @@ +{ + "averageFuelConsumption": { + "value": 4.0, + "unit": "l/100km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "averageSpeed": { + "value": 65, + "unit": "km/h", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "tripMeterManual": { + "value": 219.7, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "tripMeterAutomatic": { + "value": 0.0, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "distanceToEmptyTank": { + "value": 920, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + }, + "distanceToEmptyBattery": { + "value": 29, + "unit": "km", + "timestamp": "2025-08-07T20:29:18.343Z" + } +} diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json b/tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json new file mode 100644 index 00000000000..734672eb59e --- /dev/null +++ b/tests/components/volvo/fixtures/xc60_phev_2020/vehicle.json @@ -0,0 +1,17 @@ +{ + "vin": "YV1ABCDEFG1234567", + "modelYear": 2020, + "gearbox": "AUTOMATIC", + "fuelType": "PETROL/ELECTRIC", + "externalColour": "Bright Silver", + "batteryCapacityKWH": 11.832, + "images": { + "exteriorImageUrl": "https://cas.volvocars.com/image/dynamic/MY20_0000/123/exterior-v1/_/default.png?market=se&client=public-api-engineering&angle=1&bg=00000000&w=1920", + "internalImageUrl": "https://cas.volvocars.com/image/dynamic/MY20_0000/123/interior-v1/_/default.jpg?market=se&client=public-api-engineering&angle=0&w=1920" + }, + "descriptions": { + "model": "XC60", + "upholstery": "CHARCOAL/LEABR3/CHARC/SPO", + "steering": "LEFT" + } +} diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index d5346cf9cd8..9d709a27fc3 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -52,7 +52,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '38', + 'state': '90.0', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_battery_capacity-entry] @@ -128,7 +128,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.volvo_ex30_car_connection', 'has_entity_name': True, 'hidden_by': None, @@ -229,63 +229,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'disconnected', - }) -# --- -# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_limit-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.volvo_ex30_charging_limit', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Charging limit', - 'platform': 'volvo', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'charging_current_limit', - 'unique_id': 'yv1abcdefg1234567_charging_current_limit', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_limit-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Volvo EX30 Charging limit', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.volvo_ex30_charging_limit', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '32', + 'state': 'connected', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-entry] @@ -341,7 +285,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power_status-entry] @@ -351,6 +295,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -390,6 +336,8 @@ 'device_class': 'enum', 'friendly_name': 'Volvo EX30 Charging power status', 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -399,7 +347,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'no_power_available', + 'state': 'fault', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_status-entry] @@ -465,7 +413,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'idle', + 'state': 'done', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_type-entry] @@ -525,7 +473,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'none', + 'state': 'ac', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_empty_battery-entry] @@ -581,7 +529,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '90', + 'state': '250', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_service-entry] @@ -693,7 +641,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '2', }) # --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_odometer-entry] @@ -1052,6 +1000,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, @@ -1268,7 +1219,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.volvo_s90_car_connection', 'has_entity_name': True, 'hidden_by': None, @@ -1784,6 +1735,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, @@ -2053,7 +2007,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.volvo_xc40_car_connection', 'has_entity_name': True, 'hidden_by': None, @@ -2266,7 +2220,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '1386', }) # --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_charging_power_status-entry] @@ -2276,6 +2230,8 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -2315,6 +2271,8 @@ 'device_class': 'enum', 'friendly_name': 'Volvo XC40 Charging power status', 'options': list([ + 'fault', + 'power_available_but_not_activated', 'providing_power', 'no_power_available', ]), @@ -2506,7 +2464,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '220', + 'state': '250', }) # --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_distance_to_service-entry] @@ -2977,6 +2935,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, @@ -3117,6 +3078,972 @@ 'state': '3822.9', }) # --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'yv1abcdefg1234567_battery_charge_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Volvo XC60 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '87.3', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery_capacity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.volvo_xc60_battery_capacity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery capacity', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_capacity', + 'unique_id': 'yv1abcdefg1234567_battery_capacity', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_battery_capacity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Volvo XC60 Battery capacity', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_battery_capacity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11.832', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_car_connection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'available', + 'car_in_use', + 'no_internet', + 'ota_installation_in_progress', + 'power_saving_mode', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.volvo_xc60_car_connection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Car connection', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'availability', + 'unique_id': 'yv1abcdefg1234567_availability', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_car_connection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC60 Car connection', + 'options': list([ + 'available', + 'car_in_use', + 'no_internet', + 'ota_installation_in_progress', + 'power_saving_mode', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_car_connection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'available', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_connection_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'connected', + 'disconnected', + 'fault', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_charging_connection_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging connection status', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charger_connection_status', + 'unique_id': 'yv1abcdefg1234567_charger_connection_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC60 Charging connection status', + 'options': list([ + 'connected', + 'disconnected', + 'fault', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_charging_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disconnected', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging', + 'discharging', + 'done', + 'error', + 'idle', + 'scheduled', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_charging_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging status', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_status', + 'unique_id': 'yv1abcdefg1234567_charging_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_charging_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC60 Charging status', + 'options': list([ + 'charging', + 'discharging', + 'done', + 'error', + 'idle', + 'scheduled', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_charging_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_distance_to_empty_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance to empty battery', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'distance_to_empty_battery', + 'unique_id': 'yv1abcdefg1234567_distance_to_empty_battery', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Distance to empty battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_distance_to_empty_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_tank-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_distance_to_empty_tank', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance to empty tank', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'distance_to_empty_tank', + 'unique_id': 'yv1abcdefg1234567_distance_to_empty_tank', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_tank-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Distance to empty tank', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_distance_to_empty_tank', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '920', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_distance_to_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance to service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'distance_to_service', + 'unique_id': 'yv1abcdefg1234567_distance_to_service', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Distance to service', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_distance_to_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29000', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_fuel_amount-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_fuel_amount', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fuel amount', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'fuel_amount', + 'unique_id': 'yv1abcdefg1234567_fuel_amount', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_fuel_amount-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_storage', + 'friendly_name': 'Volvo XC60 Fuel amount', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_fuel_amount', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '47.3', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_odometer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_odometer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Odometer', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'odometer', + 'unique_id': 'yv1abcdefg1234567_odometer', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_odometer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Odometer', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_odometer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30000', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target battery charge level', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'target_battery_charge_level', + 'unique_id': 'yv1abcdefg1234567_target_battery_charge_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC60 Target battery charge level', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_time_to_engine_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time to engine service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'engine_time_to_service', + 'unique_id': 'yv1abcdefg1234567_engine_time_to_service', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Volvo XC60 Time to engine service', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_time_to_engine_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1266', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_time_to_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time to service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time_to_service', + 'unique_id': 'yv1abcdefg1234567_time_to_service', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Volvo XC60 Time to service', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_time_to_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '690', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_automatic_distance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_trip_automatic_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Trip automatic distance', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'trip_meter_automatic', + 'unique_id': 'yv1abcdefg1234567_trip_meter_automatic', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_automatic_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Trip automatic distance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_automatic_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_fuel_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_fuel_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Trip manual average fuel consumption', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'average_fuel_consumption', + 'unique_id': 'yv1abcdefg1234567_average_fuel_consumption', + 'unit_of_measurement': 'L/100 km', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_fuel_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC60 Trip manual average fuel consumption', + 'state_class': , + 'unit_of_measurement': 'L/100 km', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_fuel_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.0', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Trip manual average speed', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'average_speed', + 'unique_id': 'yv1abcdefg1234567_average_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_average_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'Volvo XC60 Trip manual average speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_manual_average_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_distance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_trip_manual_distance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Trip manual distance', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'trip_meter_manual', + 'unique_id': 'yv1abcdefg1234567_trip_meter_manual', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_trip_manual_distance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Volvo XC60 Trip manual distance', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_trip_manual_distance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '219.7', + }) +# --- # name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3193,7 +4120,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.volvo_xc90_car_connection', 'has_entity_name': True, 'hidden_by': None, @@ -3709,6 +4636,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': None, 'original_icon': None, diff --git a/tests/components/volvo/test_config_flow.py b/tests/components/volvo/test_config_flow.py index 91a7803dce5..3129b1383fe 100644 --- a/tests/components/volvo/test_config_flow.py +++ b/tests/components/volvo/test_config_flow.py @@ -13,7 +13,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components.volvo.const import CONF_VIN, DOMAIN from homeassistant.config_entries import ConfigFlowResult -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow @@ -117,6 +117,53 @@ async def test_reauth_flow( assert result["reason"] == "reauth_successful" +@pytest.mark.usefixtures("current_request_with_host") +async def test_reauth_no_stale_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + mock_config_flow_api: VolvoCarsApi, +) -> None: + """Test if reauthentication flow does not use stale data.""" + old_access_token = mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] + + with patch( + "homeassistant.components.volvo.config_flow._create_volvo_cars_api", + return_value=mock_config_flow_api, + ) as mock_create_volvo_cars_api: + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URI, + }, + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + result = await _async_run_flow_to_completion( + hass, + result, + mock_config_flow_api, + has_vin_step=False, + is_reauth=True, + ) + + assert mock_create_volvo_cars_api.called + call = mock_create_volvo_cars_api.call_args_list[0] + access_token_arg = call.args[1] + assert old_access_token != access_token_arg + + async def test_reconfigure_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/volvo/test_sensor.py b/tests/components/volvo/test_sensor.py index f610ee2ed57..a4b7a787117 100644 --- a/tests/components/volvo/test_sensor.py +++ b/tests/components/volvo/test_sensor.py @@ -15,7 +15,13 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.parametrize( "full_model", - ["ex30_2024", "s90_diesel_2018", "xc40_electric_2024", "xc90_petrol_2019"], + [ + "ex30_2024", + "s90_diesel_2018", + "xc40_electric_2024", + "xc60_phev_2020", + "xc90_petrol_2019", + ], ) async def test_sensor( hass: HomeAssistant, @@ -30,3 +36,52 @@ async def test_sensor( assert await setup_integration() await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "full_model", + ["xc40_electric_2024"], +) +async def test_distance_to_empty_battery( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], +) -> None: + """Test using `distanceToEmptyBattery` instead of `electricRange`.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert hass.states.get("sensor.volvo_xc40_distance_to_empty_battery").state == "250" + + +@pytest.mark.parametrize( + ("full_model", "short_model"), + [("ex30_2024", "ex30"), ("xc60_phev_2020", "xc60")], +) +async def test_skip_invalid_api_fields( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], + short_model: str, +) -> None: + """Test if invalid values are not creating a sensor.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert not hass.states.get(f"sensor.volvo_{short_model}_charging_current_limit") + + +@pytest.mark.parametrize( + "full_model", + ["ex30_2024"], +) +async def test_charging_power_value( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], +) -> None: + """Test if charging_power_value is zero if supported, but not charging.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert hass.states.get("sensor.volvo_ex30_charging_power").state == "0" diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index c9214ed8b71..fbaa7519ea8 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -53,7 +53,7 @@ def mock_update_fixture(): @pytest.fixture(name="validate_config_entry") def validate_config_entry_fixture(mock_update): """Return valid config entry.""" - mock_update.return_value = None + mock_update.return_value = [] return mock_update diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index 9ff7509a52c..da718a98983 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -116,8 +116,8 @@ async def test_options(hass: HomeAssistant) -> None: CONF_AVOID_FERRIES: True, CONF_AVOID_SUBSCRIPTION_ROADS: True, CONF_AVOID_TOLL_ROADS: True, - CONF_EXCL_FILTER: ["exclude"], - CONF_INCL_FILTER: ["include"], + CONF_EXCL_FILTER: ["ExcludeThis"], + CONF_INCL_FILTER: ["IncludeThis"], CONF_REALTIME: False, CONF_UNITS: IMPERIAL_UNITS, CONF_VEHICLE_TYPE: "taxi", @@ -129,8 +129,8 @@ async def test_options(hass: HomeAssistant) -> None: CONF_AVOID_FERRIES: True, CONF_AVOID_SUBSCRIPTION_ROADS: True, CONF_AVOID_TOLL_ROADS: True, - CONF_EXCL_FILTER: ["exclude"], - CONF_INCL_FILTER: ["include"], + CONF_EXCL_FILTER: ["ExcludeThis"], + CONF_INCL_FILTER: ["IncludeThis"], CONF_REALTIME: False, CONF_UNITS: IMPERIAL_UNITS, CONF_VEHICLE_TYPE: "taxi", @@ -140,8 +140,8 @@ async def test_options(hass: HomeAssistant) -> None: CONF_AVOID_FERRIES: True, CONF_AVOID_SUBSCRIPTION_ROADS: True, CONF_AVOID_TOLL_ROADS: True, - CONF_EXCL_FILTER: ["exclude"], - CONF_INCL_FILTER: ["include"], + CONF_EXCL_FILTER: ["ExcludeThis"], + CONF_INCL_FILTER: ["IncludeThis"], CONF_REALTIME: False, CONF_UNITS: IMPERIAL_UNITS, CONF_VEHICLE_TYPE: "taxi", diff --git a/tests/components/waze_travel_time/test_init.py b/tests/components/waze_travel_time/test_init.py index 89bccc00985..d11bca524e9 100644 --- a/tests/components/waze_travel_time/test_init.py +++ b/tests/components/waze_travel_time/test_init.py @@ -101,8 +101,8 @@ async def test_migrate_entry_v1_v2(hass: HomeAssistant) -> None: CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES, CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS, CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS, - CONF_INCL_FILTER: "include", - CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "IncludeThis", + CONF_EXCL_FILTER: "ExcludeThis", }, ) @@ -114,5 +114,5 @@ async def test_migrate_entry_v1_v2(hass: HomeAssistant) -> None: assert updated_entry.state is ConfigEntryState.LOADED assert updated_entry.version == 2 - assert updated_entry.options[CONF_INCL_FILTER] == ["include"] - assert updated_entry.options[CONF_EXCL_FILTER] == ["exclude"] + assert updated_entry.options[CONF_INCL_FILTER] == ["IncludeThis"] + assert updated_entry.options[CONF_EXCL_FILTER] == ["ExcludeThis"] diff --git a/tests/components/waze_travel_time/test_sensor.py b/tests/components/waze_travel_time/test_sensor.py index 94e3a0cf9d7..0aa99196c48 100644 --- a/tests/components/waze_travel_time/test_sensor.py +++ b/tests/components/waze_travel_time/test_sensor.py @@ -18,6 +18,7 @@ from homeassistant.components.waze_travel_time.const import ( IMPERIAL_UNITS, METRIC_UNITS, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .const import MOCK_CONFIG @@ -153,5 +154,5 @@ async def test_sensor_failed_wrcerror( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get("sensor.waze_travel_time").state == "unknown" + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert "Error on retrieving data: " in caplog.text diff --git a/tests/components/websocket_api/snapshots/test_commands.ambr b/tests/components/websocket_api/snapshots/test_commands.ambr new file mode 100644 index 00000000000..e8ac80e0e24 --- /dev/null +++ b/tests/components/websocket_api/snapshots/test_commands.ambr @@ -0,0 +1,136 @@ +# serializer version: 1 +# name: test_get_services + dict({ + 'reload': dict({ + 'description': 'Reloads group configuration, entities, and notify services from YAML-configuration.', + 'fields': dict({ + }), + 'name': 'Reload', + }), + 'remove': dict({ + 'description': 'Removes a group.', + 'fields': dict({ + 'object_id': dict({ + 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', + 'example': 'test_group', + 'name': 'Object ID', + 'required': True, + 'selector': dict({ + 'object': dict({ + }), + }), + }), + }), + 'name': 'Remove', + }), + 'set': dict({ + 'description': 'Creates/Updates a group.', + 'fields': dict({ + 'add_entities': dict({ + 'description': 'List of members to be added to the group. Cannot be used in combination with `Entities` or `Remove entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Add entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + 'all': dict({ + 'description': 'Enable this option if the group should only be used when all entities are in state `on`.', + 'name': 'All', + 'selector': dict({ + 'boolean': dict({ + }), + }), + }), + 'entities': dict({ + 'description': 'List of all members in the group. Cannot be used in combination with `Add entities` or `Remove entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + 'icon': dict({ + 'description': 'Name of the icon for the group.', + 'example': 'mdi:camera', + 'name': 'Icon', + 'selector': dict({ + 'icon': dict({ + }), + }), + }), + 'name': dict({ + 'description': 'Name of the group.', + 'example': 'My test group', + 'name': 'Name', + 'selector': dict({ + 'text': dict({ + }), + }), + }), + 'object_id': dict({ + 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', + 'example': 'test_group', + 'name': 'Object ID', + 'required': True, + 'selector': dict({ + 'text': dict({ + }), + }), + }), + 'remove_entities': dict({ + 'description': 'List of members to be removed from a group. Cannot be used in combination with `Entities` or `Add entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Remove entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + }), + 'name': 'Set', + }), + }) +# --- +# name: test_get_services.1 + dict({ + 'set_default_level': dict({ + 'description': 'Translated description', + 'fields': dict({ + 'level': dict({ + 'description': 'Field description', + 'example': 'Field example', + 'name': 'Field name', + 'selector': dict({ + 'select': dict({ + 'options': list([ + 'debug', + 'info', + 'warning', + 'error', + 'fatal', + 'critical', + ]), + 'translation_key': 'level', + }), + }), + }), + }), + 'name': 'Translated name', + }), + 'set_level': dict({ + 'description': '', + 'fields': dict({ + }), + 'name': '', + }), + }) +# --- diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 846b3657bb2..bffb2959b31 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -8,10 +8,13 @@ from typing import Any from unittest.mock import ANY, AsyncMock, Mock, patch import pytest +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant import loader from homeassistant.components.device_automation import toggle_entity +from homeassistant.components.group import DOMAIN as DOMAIN_GROUP +from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -34,7 +37,7 @@ from homeassistant.helpers.event import async_track_state_change_event from homeassistant.loader import Integration, async_get_integration from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component from homeassistant.util.json import json_loads -from homeassistant.util.yaml.loader import parse_yaml +from homeassistant.util.yaml.loader import JSON_TYPE, parse_yaml from tests.common import ( MockConfigEntry, @@ -671,7 +674,9 @@ async def test_get_states( async def test_get_services( - hass: HomeAssistant, websocket_client: MockHAClientWebSocket + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + snapshot: SnapshotAssertion, ) -> None: """Test get_services command.""" assert ALL_SERVICE_DESCRIPTIONS_JSON_CACHE not in hass.data @@ -686,16 +691,18 @@ async def test_get_services( assert msg == {"id": 2, "result": {}, "success": True, "type": "result"} assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is old_cache - # Load a service and check cache is updated - assert await async_setup_component(hass, "logger", {}) + # Set up an integration that has services and check cache is updated + assert await async_setup_component(hass, DOMAIN_GROUP, {DOMAIN_GROUP: {}}) await websocket_client.send_json_auto_id({"type": "get_services"}) msg = await websocket_client.receive_json() assert msg == { "id": 3, - "result": {"logger": {"set_default_level": ANY, "set_level": ANY}}, + "result": {DOMAIN_GROUP: ANY}, "success": True, "type": "result", } + group_services = msg["result"][DOMAIN_GROUP] + assert group_services == snapshot assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is not old_cache # Check cache is reused @@ -704,12 +711,70 @@ async def test_get_services( msg = await websocket_client.receive_json() assert msg == { "id": 4, - "result": {"logger": {"set_default_level": ANY, "set_level": ANY}}, + "result": {DOMAIN_GROUP: group_services}, "success": True, "type": "result", } assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is old_cache + # Set up an integration with legacy translations in services.yaml + def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE: + return { + "set_default_level": { + "description": "Translated description", + "fields": { + "level": { + "description": "Field description", + "example": "Field example", + "name": "Field name", + "selector": { + "select": { + "options": [ + "debug", + "info", + "warning", + "error", + "fatal", + "critical", + ], + "translation_key": "level", + } + }, + } + }, + "name": "Translated name", + }, + "set_level": None, + } + + await async_setup_component(hass, DOMAIN_LOGGER, {DOMAIN_LOGGER: {}}) + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.helpers.service._load_services_file", + side_effect=_load_services_file, + ), + patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={}, + ), + ): + await websocket_client.send_json_auto_id({"type": "get_services"}) + msg = await websocket_client.receive_json() + + assert msg == { + "id": 5, + "result": { + DOMAIN_LOGGER: ANY, + DOMAIN_GROUP: group_services, + }, + "success": True, + "type": "result", + } + logger_services = msg["result"][DOMAIN_LOGGER] + assert logger_services == snapshot + @patch("annotatedyaml.loader.load_yaml") @patch.object(Integration, "has_conditions", return_value=True) diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr index f7c704a2c49..bfd56fbc4d4 100644 --- a/tests/components/withings/snapshots/test_diagnostics.ambr +++ b/tests/components/withings/snapshots/test_diagnostics.ambr @@ -1,6 +1,18 @@ # serializer version: 1 # name: test_diagnostics_cloudhook_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': True, 'has_valid_external_webhook_url': True, 'received_activity_data': False, @@ -64,6 +76,18 @@ # --- # name: test_diagnostics_polling_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': False, 'has_valid_external_webhook_url': False, 'received_activity_data': False, @@ -127,6 +151,18 @@ # --- # name: test_diagnostics_webhook_instance dict({ + 'devices': list([ + dict({ + 'battery': 'high', + 'device_id': '**REDACTED**', + 'device_type': 'Scale', + 'first_session_date': None, + 'hashed_device_id': '**REDACTED**', + 'last_session_date': '2023-09-04T22:39:39+00:00', + 'model': 5, + 'raw_model': 'Body+', + }), + ]), 'has_cloudhooks': False, 'has_valid_external_webhook_url': True, 'received_activity_data': False, diff --git a/tests/components/wyoming/snapshots/test_tts.ambr b/tests/components/wyoming/snapshots/test_tts.ambr index 67c9b24160c..53cc02eaacf 100644 --- a/tests/components/wyoming/snapshots/test_tts.ambr +++ b/tests/components/wyoming/snapshots/test_tts.ambr @@ -1,19 +1,6 @@ # serializer version: 1 # name: test_get_tts_audio list([ - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -21,29 +8,10 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- # name: test_get_tts_audio_different_formats list([ - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -51,29 +19,10 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- # name: test_get_tts_audio_different_formats.1 list([ - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -81,12 +30,6 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- # name: test_get_tts_audio_streaming @@ -128,23 +71,6 @@ # --- # name: test_voice_speaker list([ - dict({ - 'data': dict({ - 'voice': dict({ - 'name': 'voice1', - 'speaker': 'speaker1', - }), - }), - 'payload': None, - 'type': 'synthesize-start', - }), - dict({ - 'data': dict({ - 'text': 'Hello world', - }), - 'payload': None, - 'type': 'synthesize-chunk', - }), dict({ 'data': dict({ 'text': 'Hello world', @@ -156,11 +82,5 @@ 'payload': None, 'type': 'synthesize', }), - dict({ - 'data': dict({ - }), - 'payload': None, - 'type': 'synthesize-stop', - }), ]) # --- diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 3540c92682b..abfe140f6cb 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -262,7 +262,7 @@ async def test_xiaomi_hhccjcy01(hass: HomeAssistant) -> None: cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" - assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") @@ -351,7 +351,7 @@ async def test_xiaomi_hhccjcy01_not_connectable(hass: HomeAssistant) -> None: cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" - assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") @@ -438,7 +438,7 @@ async def test_xiaomi_hhccjcy01_only_some_sources_connectable( cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" - assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") @@ -653,7 +653,7 @@ async def test_hhccjcy10_uuid(hass: HomeAssistant) -> None: cond_sensor_attr = cond_sensor.attributes assert cond_sensor.state == "91" assert cond_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Conductivity" - assert cond_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm" assert cond_sensor_attr[ATTR_STATE_CLASS] == "measurement" moist_sensor = hass.states.get("sensor.plant_sensor_5bfc_moisture") diff --git a/tests/components/zha/data.py b/tests/components/zha/data.py index 80a3df524cd..11c09229eb8 100644 --- a/tests/components/zha/data.py +++ b/tests/components/zha/data.py @@ -9,6 +9,7 @@ BASE_CUSTOM_CONFIGURATION = { "valueMax": 6553.6, "name": "default_light_transition", "optional": True, + "required": False, "default": 0, }, { @@ -40,6 +41,7 @@ BASE_CUSTOM_CONFIGURATION = { "valueMin": 0, "name": "consider_unavailable_mains", "optional": True, + "required": False, "default": 7200, }, { @@ -47,6 +49,7 @@ BASE_CUSTOM_CONFIGURATION = { "valueMin": 0, "name": "consider_unavailable_battery", "optional": True, + "required": False, "default": 21600, }, { @@ -80,6 +83,7 @@ CONFIG_WITH_ALARM_OPTIONS = { "valueMax": 6553.6, "name": "default_light_transition", "optional": True, + "required": False, "default": 0, }, { @@ -111,6 +115,7 @@ CONFIG_WITH_ALARM_OPTIONS = { "valueMin": 0, "name": "consider_unavailable_mains", "optional": True, + "required": False, "default": 7200, }, { @@ -118,6 +123,7 @@ CONFIG_WITH_ALARM_OPTIONS = { "valueMin": 0, "name": "consider_unavailable_battery", "optional": True, + "required": False, "default": 21600, }, { diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 35eb320893f..4d90942fb97 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -36,6 +36,7 @@ }), 'network_key': '**REDACTED**', 'nwk_addresses': dict({ + '11:22:33:44:55:66:77:88': 4660, }), 'nwk_manager_id': 0, 'nwk_update_id': 0, diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 0e78a9a1b5b..d32dd191527 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -6,6 +6,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from zigpy.profiles import zha +from zigpy.types import EUI64, NWK from zigpy.zcl.clusters import security from homeassistant.components.zha.helpers import ( @@ -71,6 +72,10 @@ async def test_diagnostics_for_config_entry( gateway.application_controller.energy_scan.side_effect = None gateway.application_controller.energy_scan.return_value = scan + gateway.application_controller.state.network_info.nwk_addresses = { + EUI64.convert("11:22:33:44:55:66:77:88"): NWK(0x1234) + } + diagnostics_data = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index f52b403869e..9c833cde0be 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -71,12 +71,14 @@ async def test_zcl_schema_conversions(hass: HomeAssistant) -> None: "options": ["Execute if off present"], "name": "options_mask", "optional": True, + "required": False, }, { "type": "multi_select", "options": ["Execute if off"], "name": "options_override", "optional": True, + "required": False, }, ] vol_schema = voluptuous_serialize.convert( diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index eef92a7eb0a..f60c0169055 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -427,6 +427,12 @@ def fortrezz_ssa1_siren_state_fixture() -> dict[str, Any]: return load_json_object_fixture("fortrezz_ssa1_siren_state.json", DOMAIN) +@pytest.fixture(name="fortrezz_ssa2_siren_state", scope="package") +def fortrezz_ssa2_siren_state_fixture() -> dict[str, Any]: + """Load the fortrezz ssa2 siren node state fixture data.""" + return load_json_object_fixture("fortrezz_ssa2_siren_state.json", DOMAIN) + + @pytest.fixture(name="fortrezz_ssa3_siren_state", scope="package") def fortrezz_ssa3_siren_state_fixture() -> dict[str, Any]: """Load the fortrezz ssa3 siren node state fixture data.""" @@ -1218,6 +1224,14 @@ def fortrezz_ssa1_siren_fixture(client, fortrezz_ssa1_siren_state) -> Node: return node +@pytest.fixture(name="fortrezz_ssa2_siren") +def fortrezz_ssa2_siren_fixture(client, fortrezz_ssa2_siren_state) -> Node: + """Mock a fortrezz ssa2 siren node.""" + node = Node(client, copy.deepcopy(fortrezz_ssa2_siren_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="fortrezz_ssa3_siren") def fortrezz_ssa3_siren_fixture(client, fortrezz_ssa3_siren_state) -> Node: """Mock a fortrezz ssa3 siren node.""" diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa2_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa2_siren_state.json new file mode 100644 index 00000000000..6fc7a89046e --- /dev/null +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa2_siren_state.json @@ -0,0 +1,447 @@ +{ + "nodeId": 30, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 132, + "productId": 265, + "productType": 785, + "firmwareVersion": "1.9", + "deviceConfig": { + "filename": "/data/db/devices/0x0084/ssa1_ssa2.json", + "isEmbedded": true, + "manufacturer": "FortrezZ LLC", + "manufacturerId": 132, + "label": "SSA1/SSA2", + "description": "Siren and Strobe Alarm", + "devices": [ + { + "productType": 785, + "productId": 267 + }, + { + "productType": 787, + "productId": 264 + }, + { + "productType": 787, + "productId": 267 + }, + { + "productType": 785, + "productId": 265 + }, + { + "productType": 787, + "productId": 265 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "paramInformation": { + "_map": {} + } + }, + "label": "SSA1/SSA2", + "interviewAttempts": 0, + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0311:0x0109:1.9", + "statistics": { + "commandsTX": 23, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 22, + "rtt": 50.7, + "lastSeen": "2025-06-10T03:23:40.329Z" + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2025-06-04T08:00:19.437Z", + "protocol": 0, + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Delay Before Accept of Basic Set Off", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Delay, from the time the siren-strobe turns on", + "label": "Delay Before Accept of Basic Set Off", + "default": 0, + "min": 0, + "max": 255, + "unit": "seconds", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 265 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 785 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 132 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.9"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "2.97" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 6 + } + ], + "endpoints": [ + { + "nodeId": 30, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 52b840fb690..bab13666a29 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -198,6 +198,17 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]: client.driver.controller.data["sdkVersion"] = original_sdk_version +@pytest.fixture(name="set_country", autouse=True) +def set_country_fixture(hass: HomeAssistant) -> Generator[None]: + """Set the country for the test.""" + original_country = hass.config.country + # Set a default country to avoid asking the user to select it. + hass.config.country = "US" + yield + # Reset the country after the test. + hass.config.country = original_country + + async def test_manual(hass: HomeAssistant) -> None: """Test we create an entry with manual step.""" @@ -4601,3 +4612,324 @@ async def test_recommended_usb_discovery( } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info", "unload_entry") +async def test_addon_rf_region_new_network( + hass: HomeAssistant, + setup_entry: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test RF region selection for new network when country is None.""" + device = "/test" + hass.config.country = None + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "usb_path": device, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "rf_region" + + # Check that all expected RF regions are available + + data_schema = result["data_schema"] + assert data_schema is not None + schema = data_schema.schema + rf_region_field = schema["rf_region"] + selector_options = rf_region_field.config["options"] + + expected_regions = [ + "Australia/New Zealand", + "China", + "Europe", + "Hong Kong", + "India", + "Israel", + "Japan", + "Korea", + "Russia", + "USA", + ] + + assert selector_options == expected_regions + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"rf_region": "Europe"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + # Verify RF region was set in addon config + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "rf_region": "Europe", + } + ), + ) + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert start_addon.call_count == 1 + assert start_addon.call_args == call("core_zwave_js") + assert setup_entry.call_count == 1 + + # avoid unload entry in teardown + entry = result["result"] + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +@pytest.mark.usefixtures("supervisor", "addon_running") +async def test_addon_rf_region_migrate_network( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, + restart_addon: AsyncMock, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + get_server_version: AsyncMock, +) -> None: + """Test migration flow with add-on.""" + hass.config.country = None + version_info = get_server_version.return_value + entry = integration + assert client.connect.call_count == 1 + assert client.driver.controller.home_id == 3245146787 + assert entry.unique_id == "3245146787" + hass.config_entries.async_update_entry( + entry, + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + addon_options["device"] = "/dev/ttyUSB0" + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.controller.data["homeId"] = 3245146787 + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes") as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + data_schema = result["data_schema"] + assert data_schema is not None + assert data_schema.schema[CONF_USB_PATH] + # Ensure the old usb path is not in the list of options + with pytest.raises(InInvalid): + data_schema.schema[CONF_USB_PATH](addon_options["device"]) + + version_info.home_id = 5678 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "rf_region" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"rf_region": "Europe"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": "/test", + "rf_region": "Europe", + } + ), + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert entry.unique_id == "5678" + version_info.home_id = 3245146787 + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 4 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == "3245146787" + assert client.driver.controller.home_id == 3245146787 + + +@pytest.mark.usefixtures("supervisor", "addon_installed", "unload_entry") +@pytest.mark.parametrize(("country", "rf_region"), [("US", "Automatic"), (None, "USA")]) +async def test_addon_skip_rf_region( + hass: HomeAssistant, + setup_entry: AsyncMock, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + start_addon: AsyncMock, + country: str | None, + rf_region: str, +) -> None: + """Test RF region selection is skipped if not needed.""" + device = "/test" + addon_options["rf_region"] = rf_region + hass.config.country = country + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "usb_path": device, + }, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + # Verify RF region was set in addon config + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "rf_region": rf_region, + } + ), + ) + + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert start_addon.call_count == 1 + assert start_addon.call_args == call("core_zwave_js") + assert setup_entry.call_count == 1 + + # avoid unload entry in teardown + entry = result["result"] + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 46686be0994..fec75d1c032 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -549,7 +549,14 @@ async def test_get_action_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"type": "boolean", "name": "refresh_all_values", "optional": True}] + ) == [ + { + "type": "boolean", + "name": "refresh_all_values", + "optional": True, + "required": False, + } + ] # Test ping capabilities = await device_action.async_get_action_capabilities( @@ -608,10 +615,15 @@ async def test_get_action_capabilities( "type": "select", }, {"name": "property", "required": True, "type": "string"}, - {"name": "property_key", "optional": True, "type": "string"}, - {"name": "endpoint", "optional": True, "type": "string"}, + {"name": "property_key", "optional": True, "required": False, "type": "string"}, + {"name": "endpoint", "optional": True, "required": False, "type": "string"}, {"name": "value", "required": True, "type": "string"}, - {"type": "boolean", "name": "wait_for_result", "optional": True}, + { + "type": "boolean", + "name": "wait_for_result", + "optional": True, + "required": False, + }, ] # Test enumerated type param @@ -771,7 +783,7 @@ async def test_get_action_capabilities_meter_triggers( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"type": "string", "name": "value", "optional": True}] + ) == [{"type": "string", "name": "value", "optional": True, "required": False}] async def test_failure_scenarios( diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 17bc4cf0f5d..123191e1f3a 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -511,8 +511,8 @@ async def test_get_condition_capabilities_value( "type": "select", }, {"name": "property", "required": True, "type": "string"}, - {"name": "property_key", "optional": True, "type": "string"}, - {"name": "endpoint", "optional": True, "type": "string"}, + {"name": "property_key", "optional": True, "required": False, "type": "string"}, + {"name": "endpoint", "optional": True, "required": False, "type": "string"}, {"name": "value", "required": True, "type": "string"}, ] diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index ccc69f7723d..7ff76888ce4 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -201,10 +201,15 @@ async def test_get_trigger_capabilities_notification_notification( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == unordered( [ - {"name": "type.", "optional": True, "type": "string"}, - {"name": "label", "optional": True, "type": "string"}, - {"name": "event", "optional": True, "type": "string"}, - {"name": "event_label", "optional": True, "type": "string"}, + {"name": "type.", "optional": True, "required": False, "type": "string"}, + {"name": "label", "optional": True, "required": False, "type": "string"}, + {"name": "event", "optional": True, "required": False, "type": "string"}, + { + "name": "event_label", + "optional": True, + "required": False, + "type": "string", + }, ] ) @@ -336,8 +341,18 @@ async def test_get_trigger_capabilities_entry_control_notification( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == unordered( [ - {"name": "event_type", "optional": True, "type": "string"}, - {"name": "data_type", "optional": True, "type": "string"}, + { + "name": "event_type", + "optional": True, + "required": False, + "type": "string", + }, + { + "name": "data_type", + "optional": True, + "required": False, + "type": "string", + }, ] ) @@ -580,6 +595,7 @@ async def test_get_trigger_capabilities_node_status( { "name": "from", "optional": True, + "required": False, "options": [ ("asleep", "asleep"), ("awake", "awake"), @@ -591,6 +607,7 @@ async def test_get_trigger_capabilities_node_status( { "name": "to", "optional": True, + "required": False, "options": [ ("asleep", "asleep"), ("awake", "awake"), @@ -599,7 +616,12 @@ async def test_get_trigger_capabilities_node_status( ], "type": "select", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] @@ -781,6 +803,7 @@ async def test_get_trigger_capabilities_basic_value_notification( { "name": "value", "optional": True, + "required": False, "type": "integer", "valueMin": 0, "valueMax": 255, @@ -972,6 +995,7 @@ async def test_get_trigger_capabilities_central_scene_value_notification( { "name": "value", "optional": True, + "required": False, "type": "select", "options": [(0, "KeyPressed"), (1, "KeyReleased"), (2, "KeyHeldDown")], }, @@ -1156,6 +1180,7 @@ async def test_get_trigger_capabilities_scene_activation_value_notification( { "name": "value", "optional": True, + "required": False, "type": "integer", "valueMin": 1, "valueMax": 255, @@ -1406,10 +1431,10 @@ async def test_get_trigger_capabilities_value_updated_value( ], }, {"name": "property", "required": True, "type": "string"}, - {"name": "property_key", "optional": True, "type": "string"}, - {"name": "endpoint", "optional": True, "type": "string"}, - {"name": "from", "optional": True, "type": "string"}, - {"name": "to", "optional": True, "type": "string"}, + {"name": "property_key", "optional": True, "required": False, "type": "string"}, + {"name": "endpoint", "optional": True, "required": False, "type": "string"}, + {"name": "from", "optional": True, "required": False, "type": "string"}, + {"name": "to", "optional": True, "required": False, "type": "string"}, ] @@ -1552,6 +1577,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( { "name": "from", "optional": True, + "required": False, "valueMin": 0, "valueMax": 255, "type": "integer", @@ -1559,6 +1585,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( { "name": "to", "optional": True, + "required": False, "valueMin": 0, "valueMax": 255, "type": "integer", @@ -1600,12 +1627,14 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_enumerate { "name": "from", "optional": True, + "required": False, "options": [(0, "Disable Beeper"), (255, "Enable Beeper")], "type": "select", }, { "name": "to", "optional": True, + "required": False, "options": [(0, "Disable Beeper"), (255, "Enable Beeper")], "type": "select", }, diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 9109d6a4048..299c003aefe 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -28,7 +28,13 @@ from homeassistant.components.zwave_js.discovery_data_template import ( DynamicCurrentTempClimateDataTemplate, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, EntityCategory +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_UNKNOWN, + EntityCategory, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util @@ -151,6 +157,16 @@ async def test_lock_popp_electric_strike_lock_control( assert hass.states.get("select.node_62_current_lock_mode") is not None +async def test_fortrez_ssa2_siren( + hass: HomeAssistant, + client: MagicMock, + fortrezz_ssa2_siren: Node, + integration: MockConfigEntry, +) -> None: + """Test Fortrezz SSA2 siren gets discovered correctly.""" + assert hass.states.get("select.siren_and_strobe_alarm") is not None + + async def test_fortrez_ssa3_siren( hass: HomeAssistant, client, fortrezz_ssa3_siren, integration ) -> None: @@ -253,6 +269,7 @@ async def test_merten_507801_disabled_enitites( assert updated_entry.disabled is False +@pytest.mark.parametrize("platforms", [[Platform.BUTTON, Platform.NUMBER]]) async def test_zooz_zen72( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -324,6 +341,9 @@ async def test_zooz_zen72( assert args["value"] is True +@pytest.mark.parametrize( + "platforms", [[Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]] +) async def test_indicator_test( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index c7b41449d43..e287c9e988f 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1045,6 +1045,183 @@ async def test_last_seen_statistics_sensors( assert state.state == "2024-01-01T12:00:00+00:00" +async def test_rssi_sensor_error( + hass: HomeAssistant, + zp3111: Node, + integration: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test rssi sensor error.""" + entity_id = "sensor.4_in_1_sensor_signal_strength" + + entity_registry.async_update_entity(entity_id, disabled_by=None) + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + # Fire statistics updated event for node + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 7, # baseline + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "7" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 125, # no signal detected + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 127, # not available + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unavailable" + + event = Event( + "statistics updated", + { + "source": "node", + "event": "statistics updated", + "nodeId": zp3111.node_id, + "statistics": { + "commandsTX": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 126, # receiver saturated + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [], + "routeFailedBetween": [], + }, + "lastSeen": "2024-01-01T00:00:00+0000", + }, + }, + ) + zp3111.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "unknown" + + ENERGY_PRODUCTION_ENTITY_MAP = { "energy_production_power": { "state": 1.23, diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index d7243268b9e..b78d202935d 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -167,7 +167,7 @@ async def test_update_entity_states( client.async_send_command.return_value = {"updates": []} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -186,7 +186,7 @@ async def test_update_entity_states( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -224,7 +224,7 @@ async def test_update_entity_states( client.async_send_command.return_value = {"updates": []} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=3)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=3)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -246,7 +246,7 @@ async def test_update_entity_install_raises( """Test update entity install raises exception.""" client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Test failed installation by driver @@ -279,7 +279,7 @@ async def test_update_entity_sleep( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Two nodes in total, the controller node and the zen_31 node. @@ -308,7 +308,7 @@ async def test_update_entity_dead( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Two nodes in total, the controller node and the zen_31 node. @@ -352,14 +352,14 @@ async def test_update_entity_ha_not_running( # Update should be delayed by a day because Home Assistant is not running hass.set_state(CoreState.starting) - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15)) await hass.async_block_till_done() assert client.async_send_command.call_count == 0 hass.set_state(CoreState.running) - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() # Two nodes in total, the controller node and the zen_31 node. @@ -385,7 +385,7 @@ async def test_update_entity_update_failure( assert client.async_send_command.call_count == 0 client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test") - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() entity_ids = (CONTROLLER_UPDATE_ENTITY, NODE_UPDATE_ENTITY) @@ -493,7 +493,7 @@ async def test_update_entity_progress( client.async_send_command.return_value = FIRMWARE_UPDATES driver = client.driver - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -641,7 +641,7 @@ async def test_update_entity_install_failed( driver = client.driver client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -717,7 +717,7 @@ async def test_update_entity_reload( client.async_send_command.return_value = {"updates": []} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=1)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -726,7 +726,7 @@ async def test_update_entity_reload( client.async_send_command.return_value = FIRMWARE_UPDATES - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=2)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=2)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -758,7 +758,7 @@ async def test_update_entity_reload( await hass.async_block_till_done() # Trigger another update and make sure the skipped version is still skipped - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=4)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=15, days=4)) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -793,7 +793,7 @@ async def test_update_entity_delay( assert client.async_send_command.call_count == 0 - update_interval = timedelta(minutes=5) + update_interval = timedelta(seconds=15) freezer.tick(update_interval) async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index 9fdf010eb64..130ce74dd5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1817,6 +1817,7 @@ async def mock_enable_bluetooth( def mock_bluetooth_adapters() -> Generator[None]: """Fixture to mock bluetooth adapters.""" with ( + patch("habluetooth.util.recover_adapter"), patch("bluetooth_auto_recovery.recover_adapter"), patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), @@ -1845,19 +1846,30 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]: # Late imports to avoid loading bleak unless we need it - from habluetooth import scanner as bluetooth_scanner # noqa: PLC0415 + from habluetooth import ( # noqa: PLC0415 + manager as bluetooth_manager, + scanner as bluetooth_scanner, + ) # We need to drop the stop method from the object since we patched # out start and this fixture will expire before the stop method is called # when EVENT_HOMEASSISTANT_STOP is fired. # pylint: disable-next=c-extension-no-member bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment] + + # Mock BlueZ management controller + mock_mgmt_bluetooth_ctl = Mock() + mock_mgmt_bluetooth_ctl.setup = AsyncMock(side_effect=OSError("Mocked error")) + with ( patch.object( bluetooth_scanner.OriginalBleakScanner, # pylint: disable=c-extension-no-member "start", ) as mock_bleak_scanner_start, patch.object(bluetooth_scanner, "HaScanner"), + patch.object( + bluetooth_manager, "MGMTBluetoothCtl", return_value=mock_mgmt_bluetooth_ctl + ), ): yield mock_bleak_scanner_start diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index dcd35a3aca7..329357bfca4 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -1,5 +1,7 @@ """Tests for hassfest requirements.""" +from collections.abc import Generator +from importlib.metadata import PackagePath from pathlib import Path from unittest.mock import patch @@ -7,8 +9,11 @@ import pytest from script.hassfest.model import Config, Integration from script.hassfest.requirements import ( + FORBIDDEN_PACKAGE_NAMES, PACKAGE_CHECK_PREPARE_UPDATE, PACKAGE_CHECK_VERSION_RANGE, + _packages_checked_files_cache, + check_dependency_files, check_dependency_version_range, validate_requirements_format, ) @@ -35,6 +40,19 @@ def integration(): ) +@pytest.fixture +def mock_forbidden_package_names() -> Generator[None]: + """Fixture for FORBIDDEN_PACKAGE_NAMES.""" + # pylint: disable-next=global-statement + global FORBIDDEN_PACKAGE_NAMES # noqa: PLW0603 + original = FORBIDDEN_PACKAGE_NAMES.copy() + FORBIDDEN_PACKAGE_NAMES = {"test", "tests"} + try: + yield + finally: + FORBIDDEN_PACKAGE_NAMES = original + + def test_validate_requirements_format_with_space(integration: Integration) -> None: """Test validate requirement with space around separator.""" integration.manifest["requirements"] = ["test_package == 1"] @@ -149,3 +167,155 @@ def test_dependency_version_range_prepare_update( ) == result ) + + +@pytest.mark.usefixtures("mock_forbidden_package_names") +def test_check_dependency_package_names(integration: Integration) -> None: + """Test dependency package names check for forbidden package names is working correctly.""" + package = "homeassistant" + pkg = "my_package" + + # Forbidden top level directories: test, tests + pkg_files = [ + PackagePath("my_package/__init__.py"), + PackagePath("my_package-1.0.0.dist-info/METADATA"), + PackagePath("tests/test_some_function.py"), + PackagePath("test/submodule/test_some_other_function.py"), + ] + with ( + patch( + "script.hassfest.requirements.files", return_value=pkg_files + ) as mock_files, + patch.dict(_packages_checked_files_cache, {}, clear=True), + ): + assert not _packages_checked_files_cache + assert check_dependency_files(integration, package, pkg, ()) is False + assert _packages_checked_files_cache[pkg]["top_level"] == {"tests", "test"} + assert len(integration.errors) == 2 + assert ( + f"Package {pkg} has a forbidden top level directory 'tests' in {package}" + in [x.error for x in integration.errors] + ) + assert ( + f"Package {pkg} has a forbidden top level directory 'test' in {package}" + in [x.error for x in integration.errors] + ) + integration.errors.clear() + + # Repeated call should use cache + assert check_dependency_files(integration, package, pkg, ()) is False + assert mock_files.call_count == 1 + assert len(integration.errors) == 2 + integration.errors.clear() + + # Exceptions set + pkg_files = [ + PackagePath("my_package/__init__.py"), + PackagePath("my_package.dist-info/METADATA"), + PackagePath("tests/test_some_function.py"), + ] + with ( + patch( + "script.hassfest.requirements.files", return_value=pkg_files + ) as mock_files, + patch.dict(_packages_checked_files_cache, {}, clear=True), + ): + assert not _packages_checked_files_cache + assert ( + check_dependency_files(integration, package, pkg, package_exceptions={pkg}) + is False + ) + assert _packages_checked_files_cache[pkg]["top_level"] == {"tests"} + assert len(integration.errors) == 0 + assert len(integration.warnings) == 1 + assert ( + f"Package {pkg} has a forbidden top level directory 'tests' in {package}" + in [x.error for x in integration.warnings] + ) + integration.warnings.clear() + + # Repeated call should use cache + assert ( + check_dependency_files(integration, package, pkg, package_exceptions={pkg}) + is False + ) + assert mock_files.call_count == 1 + assert len(integration.errors) == 0 + assert len(integration.warnings) == 1 + integration.warnings.clear() + + # All good + pkg_files = [ + PackagePath("my_package/__init__.py"), + PackagePath("my_package.dist-info/METADATA"), + ] + with ( + patch( + "script.hassfest.requirements.files", return_value=pkg_files + ) as mock_files, + patch.dict(_packages_checked_files_cache, {}, clear=True), + ): + assert not _packages_checked_files_cache + assert check_dependency_files(integration, package, pkg, ()) is True + assert _packages_checked_files_cache[pkg]["top_level"] == set() + assert len(integration.errors) == 0 + + # Repeated call should use cache + assert check_dependency_files(integration, package, pkg, ()) is True + assert mock_files.call_count == 1 + assert len(integration.errors) == 0 + + +def test_check_dependency_file_names(integration: Integration) -> None: + """Test dependency file name check for forbidden files is working correctly.""" + package = "homeassistant" + pkg = "my_package" + + # Forbidden file: 'py.typed' at top level + pkg_files = [ + PackagePath("py.typed"), + PackagePath("my_package.py"), + PackagePath("my_package-1.0.0.dist-info/METADATA"), + ] + with ( + patch( + "script.hassfest.requirements.files", return_value=pkg_files + ) as mock_files, + patch.dict(_packages_checked_files_cache, {}, clear=True), + ): + assert not _packages_checked_files_cache + assert check_dependency_files(integration, package, pkg, ()) is False + assert _packages_checked_files_cache[pkg]["file_names"] == {"py.typed"} + assert len(integration.errors) == 1 + assert f"Package {pkg} has a forbidden file 'py.typed' in {package}" in [ + x.error for x in integration.errors + ] + integration.errors.clear() + + # Repeated call should use cache + assert check_dependency_files(integration, package, pkg, ()) is False + assert mock_files.call_count == 1 + assert len(integration.errors) == 1 + integration.errors.clear() + + # All good + pkg_files = [ + PackagePath("my_package/__init__.py"), + PackagePath("my_package/py.typed"), + PackagePath("my_package.dist-info/METADATA"), + ] + with ( + patch( + "script.hassfest.requirements.files", return_value=pkg_files + ) as mock_files, + patch.dict(_packages_checked_files_cache, {}, clear=True), + ): + assert not _packages_checked_files_cache + assert check_dependency_files(integration, package, pkg, ()) is True + assert _packages_checked_files_cache[pkg]["file_names"] == set() + assert len(integration.errors) == 0 + + # Repeated call should use cache + assert check_dependency_files(integration, package, pkg, ()) is True + assert mock_files.call_count == 1 + assert len(integration.errors) == 0 diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index d056c25fc3b..d45c4f6cf91 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -3368,6 +3368,98 @@ async def test_cleanup_startup(hass: HomeAssistant) -> None: assert len(mock_call.mock_calls) == 1 +async def test_deleted_device_clears_disabled_by_on_config_entry_removal( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that disabled_by is cleared when config entry is removed.""" + config_entry = MockConfigEntry(domain="test", entry_id="mock-id-1") + config_entry.add_to_hass(hass) + + # Create a device disabled by the config entry + device = device_registry.async_get_or_create( + config_entry_id="mock-id-1", + identifiers={("test", "device_1")}, + name="Test Device", + disabled_by=dr.DeviceEntryDisabler.CONFIG_ENTRY, + ) + assert device.config_entries == {"mock-id-1"} + assert device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY + + # Remove the device (it moves to deleted_devices) + device_registry.async_remove_device(device.id) + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 1 + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == {"mock-id-1"} + assert deleted_device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY + assert deleted_device.orphaned_timestamp is None + + # Clear the config entry + device_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is cleared + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == set() + assert deleted_device.disabled_by is None # Should be cleared + assert deleted_device.orphaned_timestamp is not None + + # Now re-add the config entry and device to verify it can be enabled + config_entry2 = MockConfigEntry(domain="test", entry_id="mock-id-2") + config_entry2.add_to_hass(hass) + + # Re-create the device with same identifiers + device2 = device_registry.async_get_or_create( + config_entry_id="mock-id-2", + identifiers={("test", "device_1")}, + name="Test Device", + ) + assert device2.config_entries == {"mock-id-2"} + assert device2.disabled_by is None # Should not be disabled anymore + assert device2.id == device.id # Should keep the same device id + + +async def test_deleted_device_disabled_by_user_not_cleared( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, +) -> None: + """Test that disabled_by=USER is not cleared when config entry is removed.""" + config_entry = MockConfigEntry(domain="test", entry_id="mock-id-1") + config_entry.add_to_hass(hass) + + # Create a device disabled by the user + device = device_registry.async_get_or_create( + config_entry_id="mock-id-1", + identifiers={("test", "device_1")}, + name="Test Device", + disabled_by=dr.DeviceEntryDisabler.USER, + ) + assert device.config_entries == {"mock-id-1"} + assert device.disabled_by is dr.DeviceEntryDisabler.USER + + # Remove the device (it moves to deleted_devices) + device_registry.async_remove_device(device.id) + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 1 + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == {"mock-id-1"} + assert deleted_device.disabled_by is dr.DeviceEntryDisabler.USER + assert deleted_device.orphaned_timestamp is None + + # Clear the config entry + device_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is NOT cleared for USER disabled devices + deleted_device = device_registry.deleted_devices[device.id] + assert deleted_device.config_entries == set() + assert ( + deleted_device.disabled_by is dr.DeviceEntryDisabler.USER + ) # Should remain USER + assert deleted_device.orphaned_timestamp is not None + + @pytest.mark.parametrize("load_registries", [False]) async def test_cleanup_entity_registry_change( hass: HomeAssistant, mock_config_entry: MockConfigEntry diff --git a/tests/helpers/test_discovery_flow.py b/tests/helpers/test_discovery_flow.py index dde0f209706..2cb2bb38030 100644 --- a/tests/helpers/test_discovery_flow.py +++ b/tests/helpers/test_discovery_flow.py @@ -10,6 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import discovery_flow, json as json_helper from homeassistant.helpers.discovery_flow import DiscoveryKey +from homeassistant.util import json as json_util @pytest.fixture @@ -151,6 +152,6 @@ def test_discovery_key_serialize_deserialize(key: str | tuple[str]) -> None: ) serialized = json_helper.json_dumps(discovery_key_1) assert ( - discovery_flow.DiscoveryKey.from_json_dict(json_helper.json_loads(serialized)) + discovery_flow.DiscoveryKey.from_json_dict(json_util.json_loads(serialized)) == discovery_key_1 ) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index e403333d8df..89822b80039 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -782,6 +782,97 @@ async def test_deleted_entity_removing_config_entry_id( assert entity_registry.deleted_entities[("light", "hue", "1234")] == deleted_entry2 +async def test_deleted_entity_clears_disabled_by_on_config_entry_removal( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test that disabled_by is cleared when config entry is removed.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) + + # Create an entity disabled by the config entry + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=mock_config, + disabled_by=er.RegistryEntryDisabler.CONFIG_ENTRY, + ) + assert entry.config_entry_id == "mock-id-1" + assert entry.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY + + # Remove the entity (it moves to deleted_entities) + entity_registry.async_remove(entry.entity_id) + + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id == "mock-id-1" + assert deleted_entry.disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY + assert deleted_entry.orphaned_timestamp is None + + # Clear the config entry + entity_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is cleared + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id is None + assert deleted_entry.disabled_by is None # Should be cleared + assert deleted_entry.orphaned_timestamp is not None + + # Now re-add the config entry and entity to verify it can be enabled + mock_config2 = MockConfigEntry(domain="light", entry_id="mock-id-2") + mock_config2.add_to_hass(hass) + + # Re-create the entity with same unique ID + entry2 = entity_registry.async_get_or_create( + "light", "hue", "5678", config_entry=mock_config2 + ) + assert entry2.config_entry_id == "mock-id-2" + assert entry2.disabled_by is None # Should not be disabled anymore + + +async def test_deleted_entity_disabled_by_user_not_cleared( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test that disabled_by=USER is not cleared when config entry is removed.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) + + # Create an entity disabled by the user + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=mock_config, + disabled_by=er.RegistryEntryDisabler.USER, + ) + assert entry.config_entry_id == "mock-id-1" + assert entry.disabled_by is er.RegistryEntryDisabler.USER + + # Remove the entity (it moves to deleted_entities) + entity_registry.async_remove(entry.entity_id) + + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 1 + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id == "mock-id-1" + assert deleted_entry.disabled_by is er.RegistryEntryDisabler.USER + assert deleted_entry.orphaned_timestamp is None + + # Clear the config entry + entity_registry.async_clear_config_entry("mock-id-1") + + # Verify disabled_by is NOT cleared for USER disabled entities + deleted_entry = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry.config_entry_id is None + assert ( + deleted_entry.disabled_by is er.RegistryEntryDisabler.USER + ) # Should remain USER + assert deleted_entry.orphaned_timestamp is not None + + async def test_removing_config_subentry_id( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 413e7e0dc9d..26ee4c675bb 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -13,7 +13,6 @@ from unittest.mock import Mock, patch import pytest from homeassistant.core import Event, HomeAssistant, State -from homeassistant.helpers import json as json_helper from homeassistant.helpers.json import ( ExtendedJSONEncoder, JSONEncoder as DefaultHASSJSONEncoder, @@ -27,14 +26,9 @@ from homeassistant.helpers.json import ( ) from homeassistant.util import dt as dt_util from homeassistant.util.color import RGBColor -from homeassistant.util.json import ( - JSON_DECODE_EXCEPTIONS, - JSON_ENCODE_EXCEPTIONS, - SerializationError, - load_json, -) +from homeassistant.util.json import SerializationError, load_json -from tests.common import import_and_test_deprecated_constant, json_round_trip +from tests.common import json_round_trip # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} @@ -350,50 +344,3 @@ def test_find_unserializable_data() -> None: BadData(), dump=partial(json.dumps, cls=MockJSONEncoder), ) == {"$(BadData).bla": bad_data} - - -def test_deprecated_json_loads(caplog: pytest.LogCaptureFixture) -> None: - """Test deprecated json_loads function. - - It was moved from helpers to util in #88099 - """ - json_helper.json_loads("{}") - assert ( - "The deprecated function json_loads was called. It will be removed " - "in HA Core 2025.8. Use homeassistant.util.json.json_loads instead" - ) in caplog.text - - -@pytest.mark.parametrize( - ("constant_name", "replacement_name", "replacement"), - [ - ( - "JSON_DECODE_EXCEPTIONS", - "homeassistant.util.json.JSON_DECODE_EXCEPTIONS", - JSON_DECODE_EXCEPTIONS, - ), - ( - "JSON_ENCODE_EXCEPTIONS", - "homeassistant.util.json.JSON_ENCODE_EXCEPTIONS", - JSON_ENCODE_EXCEPTIONS, - ), - ], -) -def test_deprecated_aliases( - caplog: pytest.LogCaptureFixture, - constant_name: str, - replacement_name: str, - replacement: Any, -) -> None: - """Test deprecated JSON_DECODE_EXCEPTIONS and JSON_ENCODE_EXCEPTIONS constants. - - They were moved from helpers to util in #88099 - """ - import_and_test_deprecated_constant( - caplog, - json_helper, - constant_name, - replacement_name, - replacement, - "2025.8", - ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 50d9da501c5..7f5255a203b 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -563,7 +563,12 @@ def test_time_selector_schema(schema, valid_selections, invalid_selections) -> N ( {"entity_id": "sensor.abc"}, ("on", "armed"), - (None, True, 1), + (None, True, 1, ["on"]), + ), + ( + {"entity_id": "sensor.abc", "multiple": True}, + (["on"], ["on", "off"], []), + (None, True, 1, [True], [1], "on"), ), ( {"hide_states": ["unknown", "unavailable"]}, diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py index 8ae291ac0b7..4ffbca6124a 100644 --- a/tests/pylint/conftest.py +++ b/tests/pylint/conftest.py @@ -138,3 +138,24 @@ def decorator_checker_fixture(hass_decorator, linter) -> BaseChecker: type_hint_checker = hass_decorator.HassDecoratorChecker(linter) type_hint_checker.module = "homeassistant.components.pylint_test" return type_hint_checker + + +@pytest.fixture(name="hass_enforce_greek_micro_char", scope="package") +def hass_enforce_greek_micro_checker_fixture() -> ModuleType: + """Fixture to the content for the hass_enforce_greek_micro_char check.""" + return _load_plugin_from_file( + "hass_enforce_greek_micro_char", + "pylint/plugins/hass_enforce_greek_micro_char.py", + ) + + +@pytest.fixture(name="enforce_greek_micro_char_checker") +def enforce_greek_micro_char_checker_fixture( + hass_enforce_greek_micro_char, linter +) -> BaseChecker: + """Fixture to provide a hass_enforce_greek_micro_char checker.""" + enforce_greek_micro_char_checker = ( + hass_enforce_greek_micro_char.HassEnforceGreekMicroCharChecker(linter) + ) + enforce_greek_micro_char_checker.module = "homeassistant.components.pylint_test" + return enforce_greek_micro_char_checker diff --git a/tests/pylint/test_enforce_greek_micro_char.py b/tests/pylint/test_enforce_greek_micro_char.py new file mode 100644 index 00000000000..fe0abd9af5f --- /dev/null +++ b/tests/pylint/test_enforce_greek_micro_char.py @@ -0,0 +1,164 @@ +"""Tests for pylint hass_enforce_greek_micro_char plugin.""" + +from __future__ import annotations + +import astroid +from pylint.checkers import BaseChecker +from pylint.testutils.unittest_linter import UnittestLinter +from pylint.utils.ast_walker import ASTWalker +import pytest + +from . import assert_no_messages + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + # Test using the correct μ-sign \u03bc with annotation + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "μg/m³" + """, + id="good_const_with_annotation", + ), + pytest.param( + # Test using the correct μ-sign \u03bc with annotation using unicode encoding + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "\u03bcg/m³" + """, + id="good_unicode_const_with_annotation", + ), + pytest.param( + # Test using the correct μ-sign \u03bc without annotation + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "μg/m³" + """, + id="good_const_without_annotation", + ), + pytest.param( + # Test using the correct μ-sign \u03bc in a StrEnum class + """ + class UnitOfElectricPotential(StrEnum): + \"\"\"Electric potential units.\"\"\" + + MICROVOLT = "μV" + MILLIVOLT = "mV" + VOLT = "V" + KILOVOLT = "kV" + MEGAVOLT = "MV" + """, + id="good_str_enum", + ), + pytest.param( + # Test using the correct μ-sign \u03bc in a sensor description dict + """ + SENSOR_DESCRIPTION = { + "radiation_rate": AranetSensorEntityDescription( + key="radiation_rate", + translation_key="radiation_rate", + name="Radiation Dose Rate", + native_unit_of_measurement="μSv/h", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + scale=0.001, + ), + } + OTHER_DICT = { + "value_with_bad_mu_should_pass": "µ" + } + """, + id="good_sensor_description", + ), + ], +) +def test_enforce_greek_micro_char( + linter: UnittestLinter, + enforce_greek_micro_char_checker: BaseChecker, + code: str, +) -> None: + """Good test cases.""" + root_node = astroid.parse(code, "homeassistant.components.pylint_test") + walker = ASTWalker(linter) + walker.add_checker(enforce_greek_micro_char_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + # Test we can detect the legacy coding of μ \u00b5 + # instead of recommended coding of μ \u03bc" with annotation + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" + """, + id="bad_const_with_annotation", + ), + pytest.param( + # Test we can detect the unicode variant of the legacy coding of μ \u00b5 + # instead of recommended coding of μ \u03bc" with annotation + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "\u00b5g/m³" + """, + id="bad_unicode_const_with_annotation", + ), + pytest.param( + # Test we can detect the legacy coding of μ \u00b5 + # instead of recommended coding of μ \u03bc" without annotation + """ + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "µg/m³" + """, + id="bad_const_without_annotation", + ), + pytest.param( + # Test we can detect the legacy coding of μ \u00b5 + # instead of recommended coding of μ \u03bc" in a StrEnum class + """ + class UnitOfElectricPotential(StrEnum): + \"\"\"Electric potential units.\"\"\" + + MICROVOLT = "µV" + MILLIVOLT = "mV" + VOLT = "V" + KILOVOLT = "kV" + MEGAVOLT = "MV" + """, + id="bad_str_enum", + ), + pytest.param( + # Test we can detect the legacy coding of μ \u00b5 + # instead of recommended coding of μ \u03bc" in a sensor description dict + """ + SENSOR_DESCRIPTION = { + "radiation_rate": AranetSensorEntityDescription( + key="radiation_rate", + translation_key="radiation_rate", + name="Radiation Dose Rate", + native_unit_of_measurement="µSv/h", + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + scale=0.001, + ), + } + """, + id="bad_sensor_description", + ), + ], +) +def test_enforce_greek_micro_char_assign_bad( + linter: UnittestLinter, + enforce_greek_micro_char_checker: BaseChecker, + code: str, +) -> None: + """Bad assignment test cases.""" + root_node = astroid.parse(code, "homeassistant.components.pylint_test") + walker = ASTWalker(linter) + walker.add_checker(enforce_greek_micro_char_checker) + + walker.walk(root_node) + messages = linter.release_messages() + assert len(messages) == 1 + message = next(iter(messages)) + assert message.msg_id == "hass-enforce-greek-micro-char" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 833d28ecdd9..9a62fd421b7 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6519,6 +6519,275 @@ async def test_update_subentry_and_abort( assert result["reason"] == "reconfigure_successful" +@pytest.mark.parametrize( + ( + "kwargs", + "expected_title", + "expected_unique_id", + "expected_data", + "raises", + "reload", # True is default + "setup_call_count", + "expected_result", + ), + [ + ( + { + "unique_id": "5678", + "title": "Updated title", + "data": {"vendor": "data2"}, + }, + "Updated title", + "5678", + {"vendor": "data2"}, + does_not_raise(), + True, + 2, + { + "type": FlowResultType.ABORT, + "reason": "reconfigure_successful", + "description_placeholders": None, + }, + ), + ( + { + "unique_id": "1234", + "title": "Test", + "data": {"vendor": "data"}, + }, + "Test", + "1234", + {"vendor": "data"}, + does_not_raise(), + True, + 2, + { + "type": FlowResultType.ABORT, + "reason": "reconfigure_successful", + "description_placeholders": None, + }, + ), + ( + { + "unique_id": "1234", + "title": "Test", + "data": {"vendor": "data"}, + }, + "Test", + "1234", + {"vendor": "data"}, + does_not_raise(), + False, + 1, + { + "type": FlowResultType.ABORT, + "reason": "reconfigure_successful", + "description_placeholders": None, + }, + ), + ( + {}, + "Test", + "1234", + {"vendor": "data"}, + does_not_raise(), + True, + 2, + { + "type": FlowResultType.ABORT, + "reason": "reconfigure_successful", + "description_placeholders": None, + }, + ), + ( + { + "data": {"buyer": "me"}, + }, + "Test", + "1234", + {"buyer": "me"}, + does_not_raise(), + True, + 2, + { + "type": FlowResultType.ABORT, + "reason": "reconfigure_successful", + "description_placeholders": None, + }, + ), + ( + {"data_updates": {"buyer": "me"}}, + "Test", + "1234", + {"vendor": "data", "buyer": "me"}, + does_not_raise(), + True, + 2, + { + "type": FlowResultType.ABORT, + "reason": "reconfigure_successful", + "description_placeholders": None, + }, + ), + ( + { + "unique_id": "5678", + "title": "Updated title", + "data": {"vendor": "data2"}, + "data_updates": {"buyer": "me"}, + }, + "Test", + "1234", + {"vendor": "data"}, + pytest.raises(ValueError), + True, + 1, + {}, + ), + ], + ids=[ + "changed_entry_default", + "unchanged_entry_default", + "unchanged_entry_no_reload", + "no_kwargs", + "replace_data", + "update_data", + "update_and_data_raises", + ], +) +async def test_update_subentry_reload_and_abort( + hass: HomeAssistant, + expected_title: str, + expected_unique_id: str, + expected_data: dict[str, Any], + kwargs: dict[str, Any], + raises: AbstractContextManager, + reload: bool, + setup_call_count: int, + expected_result: dict[str, Any], +) -> None: + """Test updating an entry and reloading.""" + subentry_id = "blabla" + entry = MockConfigEntry( + domain="comp", + unique_id="entry_unique_id", + title="entry_title", + data={}, + subentries_data=[ + config_entries.ConfigSubentryData( + data={"vendor": "data"}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="1234", + title="Test", + ) + ], + ) + entry.add_to_hass(hass) + subentry = entry.subentries[subentry_id] + + setup_entry = AsyncMock(return_value=True) + + comp = MockModule( + "comp", + async_setup_entry=setup_entry, + async_unload_entry=AsyncMock(return_value=True), + ) + mock_integration(hass, comp) + mock_platform(hass, "comp.config_flow", None) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + class TestFlow(config_entries.ConfigFlow): + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + async def async_step_reconfigure(self, user_input=None): + return self.async_update_reload_and_abort( + self._get_entry(), + self._get_reconfigure_subentry(), + **kwargs, + reload_even_if_entry_is_unchanged=reload, + ) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: config_entries.ConfigEntry + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + with mock_config_flow("comp", TestFlow), raises: + result = await entry.start_subentry_reconfigure_flow(hass, subentry_id) + + await hass.async_block_till_done() + + subentry = entry.subentries[subentry_id] + assert subentry.title == expected_title + assert subentry.unique_id == expected_unique_id + assert subentry.data == expected_data + assert setup_entry.call_count == setup_call_count + for k, v in expected_result.items(): + assert result[k] == v + + +async def test_update_subentry_reload_with_listener(hass: HomeAssistant) -> None: + """Test updating an entry and reloading fails with update listener.""" + subentry_id = "blabla" + entry = MockConfigEntry( + domain="comp", + unique_id="entry_unique_id", + title="entry_title", + data={}, + subentries_data=[ + config_entries.ConfigSubentryData( + data={"vendor": "data"}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="1234", + title="Test", + ) + ], + ) + entry.add_to_hass(hass) + entry.add_update_listener(AsyncMock()) + + setup_entry = AsyncMock(return_value=True) + + comp = MockModule( + "comp", + async_setup_entry=setup_entry, + async_unload_entry=AsyncMock(return_value=True), + ) + mock_integration(hass, comp) + mock_platform(hass, "comp.config_flow", None) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + class TestFlow(config_entries.ConfigFlow): + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + async def async_step_reconfigure(self, user_input=None): + return self.async_update_reload_and_abort( + self._get_entry(), + self._get_reconfigure_subentry(), + data={}, + reload_even_if_entry_is_unchanged=True, + ) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: config_entries.ConfigEntry + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + with ( + mock_config_flow("comp", TestFlow), + pytest.raises( + ValueError, match="Cannot update and reload entry with update listeners" + ), + ): + await entry.start_subentry_reconfigure_flow(hass, subentry_id) + + async def test_reconfigure_subentry_create_subentry(hass: HomeAssistant) -> None: """Test it's not allowed to create a subentry from a subentry reconfigure flow.""" subentry_id = "blabla" diff --git a/tests/test_const.py b/tests/test_const.py index f1ceaad6a08..3398a571f6f 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -118,7 +118,7 @@ def test_deprecated_unit_of_conductivity_alias() -> None: """Test UnitOfConductivity deprecation.""" # Test the deprecated members are aliases - assert set(const.UnitOfConductivity) == {"S/cm", "µS/cm", "mS/cm"} + assert set(const.UnitOfConductivity) == {"S/cm", "μS/cm", "mS/cm"} def test_deprecated_unit_of_conductivity_members( diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 0faa4dd1a80..f0912188b9e 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -1229,7 +1229,13 @@ def test_section_in_serializer() -> None: ) == { "expanded": True, "schema": [ - {"default": False, "name": "option_1", "optional": True, "type": "boolean"}, + { + "default": False, + "name": "option_1", + "optional": True, + "required": False, + "type": "boolean", + }, {"name": "option_2", "required": True, "type": "integer"}, ], "type": "expandable", diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 537cfb33c31..d6f9d282174 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, + UnitOfApparentPower, UnitOfArea, UnitOfBloodGlucoseConcentration, UnitOfConductivity, @@ -28,6 +29,7 @@ from homeassistant.const import ( UnitOfPower, UnitOfPressure, UnitOfReactiveEnergy, + UnitOfReactivePower, UnitOfSpeed, UnitOfTemperature, UnitOfTime, @@ -38,6 +40,7 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.util import unit_conversion from homeassistant.util.unit_conversion import ( + ApparentPowerConverter, AreaConverter, BaseUnitConverter, BloodGlucoseConcentrationConverter, @@ -55,6 +58,7 @@ from homeassistant.util.unit_conversion import ( PowerConverter, PressureConverter, ReactiveEnergyConverter, + ReactivePowerConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -83,9 +87,11 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { EnergyConverter, InformationConverter, MassConverter, + ApparentPowerConverter, PowerConverter, PressureConverter, ReactiveEnergyConverter, + ReactivePowerConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -97,6 +103,11 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { # Dict containing all converters with a corresponding unit ratio. _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = { + ApparentPowerConverter: ( + UnitOfApparentPower.MILLIVOLT_AMPERE, + UnitOfApparentPower.VOLT_AMPERE, + 1000, + ), AreaConverter: (UnitOfArea.SQUARE_KILOMETERS, UnitOfArea.SQUARE_METERS, 0.000001), BloodGlucoseConcentrationConverter: ( UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, @@ -145,6 +156,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, 1000, ), + ReactivePowerConverter: ( + UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE, + UnitOfReactivePower.VOLT_AMPERE_REACTIVE, + 1000, + ), SpeedConverter: ( UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR, @@ -168,6 +184,14 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo _CONVERTED_VALUE: dict[ type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]] ] = { + ApparentPowerConverter: [ + ( + 10, + UnitOfApparentPower.MILLIVOLT_AMPERE, + 0.01, + UnitOfApparentPower.VOLT_AMPERE, + ), + ], AreaConverter: [ # Square Meters to other units (5, UnitOfArea.SQUARE_METERS, 50000, UnitOfArea.SQUARE_CENTIMETERS), @@ -666,6 +690,32 @@ _CONVERTED_VALUE: dict[ UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, ), ], + ReactivePowerConverter: [ + ( + 10, + UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE, + 10000, + UnitOfReactivePower.VOLT_AMPERE_REACTIVE, + ), + ( + 10, + UnitOfReactivePower.VOLT_AMPERE_REACTIVE, + 0.01, + UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE, + ), + ( + 10, + UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE, + 0.01, + UnitOfReactivePower.VOLT_AMPERE_REACTIVE, + ), + ( + 10, + UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE, + 0.00001, + UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE, + ), + ], SpeedConverter: [ # 5 km/h / 1.609 km/mi = 3.10686 mi/h (5, UnitOfSpeed.KILOMETERS_PER_HOUR, 3.106856, UnitOfSpeed.MILES_PER_HOUR), @@ -897,6 +947,12 @@ _CONVERTED_VALUE: dict[ 1, UnitOfVolumeFlowRate.LITERS_PER_SECOND, ), + ( + 0.6, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_MINUTE, + 10, + UnitOfVolumeFlowRate.LITERS_PER_SECOND, + ), ], }