mirror of
https://github.com/home-assistant/core.git
synced 2026-05-28 19:53:18 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51cddb88f5 | |||
| 748a9842af | |||
| 55786dbdfc | |||
| e88c03a437 | |||
| dbc0dc1ea6 | |||
| 31271876bf | |||
| d5c31332b5 | |||
| 3f0c93c26c | |||
| 07ed913ba2 | |||
| b7905b163f | |||
| c712b07da3 |
@@ -24,7 +24,6 @@ The following platforms have extra guidelines:
|
|||||||
## Entity platforms
|
## Entity platforms
|
||||||
|
|
||||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||||
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
|
|
||||||
|
|
||||||
## Integration Quality Scale
|
## Integration Quality Scale
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
name: Set up uv and managed Python
|
||||||
|
description: >-
|
||||||
|
Pins uv (avoids the raw.githubusercontent.com manifest fetch on cache miss)
|
||||||
|
and proactively installs the requested Python so cached venvs created with
|
||||||
|
`uv venv` resolve their interpreter symlinks in jobs that only restore the
|
||||||
|
venv. setup-uv alone only sets UV_PYTHON, it does not actually fetch the
|
||||||
|
interpreter until uv first uses it, so jobs that just activate the venv
|
||||||
|
blow up with broken symlinks on cache hit.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
python-version:
|
||||||
|
description: The Python version uv should install and use.
|
||||||
|
required: true
|
||||||
|
uv-version:
|
||||||
|
description: The uv version setup-uv should install.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
python-version:
|
||||||
|
description: The Python version uv reports as installed.
|
||||||
|
value: ${{ steps.uv.outputs.python-version }}
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Set up uv
|
||||||
|
id: uv
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
version: ${{ inputs.uv-version }}
|
||||||
|
python-version: ${{ inputs.python-version }}
|
||||||
|
# Persist astral's managed Python across jobs so 'uv venv' below is
|
||||||
|
# fast on the second job onwards.
|
||||||
|
cache-python: true
|
||||||
|
# Lint-only and codegen jobs touch no Python deps, so the post-step
|
||||||
|
# cache save would otherwise abort the job.
|
||||||
|
ignore-nothing-to-cache: true
|
||||||
|
- name: Install Python interpreter
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PYTHON_VERSION: ${{ inputs.python-version }}
|
||||||
|
run: uv python install "${PYTHON_VERSION}"
|
||||||
@@ -43,7 +43,6 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
|||||||
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
|
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
|
||||||
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body. Use `pytest.param` with an `id` parameter to name the test cases clearly.
|
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body. Use `pytest.param` with an `id` parameter to name the test cases clearly.
|
||||||
- We use Syrupy for snapshot testing. Leverage `.ambr` snapshots instead of repetitive and exhaustive generation of test data within Python code itself.
|
- We use Syrupy for snapshot testing. Leverage `.ambr` snapshots instead of repetitive and exhaustive generation of test data within Python code itself.
|
||||||
- Hardcoded `entity_id`s in tests are fine. If the same one is repeated, use a constant.
|
|
||||||
|
|
||||||
## Good practices
|
## Good practices
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ The following platforms have extra guidelines:
|
|||||||
## Entity platforms
|
## Entity platforms
|
||||||
|
|
||||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||||
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
|
|
||||||
|
|
||||||
## Integration Quality Scale
|
## Integration Quality Scale
|
||||||
|
|
||||||
|
|||||||
@@ -530,7 +530,7 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
@@ -543,7 +543,7 @@ jobs:
|
|||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
|
|||||||
+7
-7
@@ -36,7 +36,7 @@
|
|||||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
# - github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
# - github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
|
||||||
#
|
#
|
||||||
# Container images used:
|
# Container images used:
|
||||||
# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
|
# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
id: setup
|
id: setup
|
||||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
job-name: ${{ github.job }}
|
job-name: ${{ github.job }}
|
||||||
@@ -352,7 +352,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
id: setup
|
id: setup
|
||||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
job-name: ${{ github.job }}
|
job-name: ${{ github.job }}
|
||||||
@@ -961,7 +961,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
id: setup
|
id: setup
|
||||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
job-name: ${{ github.job }}
|
job-name: ${{ github.job }}
|
||||||
@@ -1100,7 +1100,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
id: setup
|
id: setup
|
||||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
job-name: ${{ github.job }}
|
job-name: ${{ github.job }}
|
||||||
@@ -1325,7 +1325,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
id: setup
|
id: setup
|
||||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
job-name: ${{ github.job }}
|
job-name: ${{ github.job }}
|
||||||
@@ -1383,7 +1383,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Scripts
|
- name: Setup Scripts
|
||||||
id: setup
|
id: setup
|
||||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
|
||||||
with:
|
with:
|
||||||
destination: ${{ runner.temp }}/gh-aw/actions
|
destination: ${{ runner.temp }}/gh-aw/actions
|
||||||
job-name: ${{ github.job }}
|
job-name: ${{ github.job }}
|
||||||
|
|||||||
+46
-48
@@ -37,9 +37,9 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 3
|
CACHE_VERSION: 4
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2026.7"
|
HA_SHORT_VERSION: "2026.6"
|
||||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
@@ -89,6 +89,8 @@ jobs:
|
|||||||
mariadb_groups: ${{ steps.info.outputs.mariadb_groups }}
|
mariadb_groups: ${{ steps.info.outputs.mariadb_groups }}
|
||||||
postgresql_groups: ${{ steps.info.outputs.postgresql_groups }}
|
postgresql_groups: ${{ steps.info.outputs.postgresql_groups }}
|
||||||
python_versions: ${{ steps.info.outputs.python_versions }}
|
python_versions: ${{ steps.info.outputs.python_versions }}
|
||||||
|
default_python: ${{ steps.info.outputs.default_python }}
|
||||||
|
uv_version: ${{ steps.info.outputs.uv_version }}
|
||||||
test_full_suite: ${{ steps.info.outputs.test_full_suite }}
|
test_full_suite: ${{ steps.info.outputs.test_full_suite }}
|
||||||
test_group_count: ${{ steps.info.outputs.test_group_count }}
|
test_group_count: ${{ steps.info.outputs.test_group_count }}
|
||||||
test_groups: ${{ steps.info.outputs.test_groups }}
|
test_groups: ${{ steps.info.outputs.test_groups }}
|
||||||
@@ -235,6 +237,11 @@ jobs:
|
|||||||
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
|
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
|
||||||
echo "python_versions: ${all_python_versions}"
|
echo "python_versions: ${all_python_versions}"
|
||||||
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
|
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
|
||||||
|
echo "default_python: ${default_python}"
|
||||||
|
echo "default_python=${default_python}" >> $GITHUB_OUTPUT
|
||||||
|
uv_version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)
|
||||||
|
echo "uv_version: ${uv_version}"
|
||||||
|
echo "uv_version=${uv_version}" >> $GITHUB_OUTPUT
|
||||||
echo "test_full_suite: ${test_full_suite}"
|
echo "test_full_suite: ${test_full_suite}"
|
||||||
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
|
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
|
||||||
echo "integrations_glob: ${integrations_glob}"
|
echo "integrations_glob: ${integrations_glob}"
|
||||||
@@ -344,12 +351,12 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up uv and Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -397,21 +404,13 @@ jobs:
|
|||||||
libudev-dev
|
libudev-dev
|
||||||
version: ${{ env.APT_CACHE_VERSION }}
|
version: ${{ env.APT_CACHE_VERSION }}
|
||||||
execute_install_scripts: true
|
execute_install_scripts: true
|
||||||
- name: Read uv version from requirements.txt
|
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
|
||||||
id: read-uv-version
|
|
||||||
run: |
|
|
||||||
echo "version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Set up uv
|
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
||||||
with:
|
|
||||||
version: ${{ steps.read-uv-version.outputs.version }}
|
|
||||||
- name: Create Python virtual environment
|
- name: Create Python virtual environment
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
id: create-venv
|
id: create-venv
|
||||||
|
env:
|
||||||
|
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
|
||||||
run: |
|
run: |
|
||||||
python -m venv venv
|
uv venv venv --python "${PYTHON_VERSION}"
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
uv pip install -r requirements.txt
|
uv pip install -r requirements.txt
|
||||||
@@ -419,7 +418,6 @@ jobs:
|
|||||||
uv pip install -e . --config-settings editable_mode=compat
|
uv pip install -e . --config-settings editable_mode=compat
|
||||||
- name: Dump pip freeze
|
- name: Dump pip freeze
|
||||||
run: |
|
run: |
|
||||||
python -m venv venv
|
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
uv pip freeze >> pip_freeze.txt
|
uv pip freeze >> pip_freeze.txt
|
||||||
@@ -480,10 +478,10 @@ jobs:
|
|||||||
version: ${{ env.APT_CACHE_VERSION }}
|
version: ${{ env.APT_CACHE_VERSION }}
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
check-latest: true
|
python-version: ${{ needs.info.outputs.default_python }}
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -517,10 +515,10 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
check-latest: true
|
python-version: ${{ needs.info.outputs.default_python }}
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -553,10 +551,10 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
check-latest: true
|
python-version: ${{ needs.info.outputs.default_python }}
|
||||||
- name: Run gen_copilot_instructions.py
|
- name: Run gen_copilot_instructions.py
|
||||||
run: |
|
run: |
|
||||||
python -m script.gen_copilot_instructions validate
|
python -m script.gen_copilot_instructions validate
|
||||||
@@ -608,10 +606,10 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -659,10 +657,10 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
check-latest: true
|
python-version: ${{ needs.info.outputs.default_python }}
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -712,10 +710,10 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
check-latest: true
|
python-version: ${{ needs.info.outputs.default_python }}
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -763,10 +761,10 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
check-latest: true
|
python-version: ${{ needs.info.outputs.default_python }}
|
||||||
- name: Generate partial mypy restore key
|
- name: Generate partial mypy restore key
|
||||||
id: generate-mypy-key
|
id: generate-mypy-key
|
||||||
run: |
|
run: |
|
||||||
@@ -840,10 +838,10 @@ jobs:
|
|||||||
execute_install_scripts: true
|
execute_install_scripts: true
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
check-latest: true
|
python-version: ${{ needs.info.outputs.default_python }}
|
||||||
- name: Restore full Python virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -905,10 +903,10 @@ jobs:
|
|||||||
execute_install_scripts: true
|
execute_install_scripts: true
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -1047,10 +1045,10 @@ jobs:
|
|||||||
execute_install_scripts: true
|
execute_install_scripts: true
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -1203,10 +1201,10 @@ jobs:
|
|||||||
version: ${{ env.APT_CACHE_VERSION }}
|
version: ${{ env.APT_CACHE_VERSION }}
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@@ -1371,10 +1369,10 @@ jobs:
|
|||||||
execute_install_scripts: true
|
execute_install_scripts: true
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: ./.github/actions/setup-uv-python
|
||||||
with:
|
with:
|
||||||
|
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
issues: write # To lock issues
|
issues: write # To lock issues
|
||||||
pull-requests: write # To lock pull requests
|
pull-requests: write # To lock pull requests
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@851cffe46851ddd2051ea7147ebdc995113241c3 # v6.0.1
|
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.15.14
|
rev: v0.15.13
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args:
|
args:
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
|||||||
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
|
- Avoid using conditions/branching in tests. Instead, either split tests or adjust the test parametrization to cover all cases without branching.
|
||||||
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body. Use `pytest.param` with an `id` parameter to name the test cases clearly.
|
- If multiple tests share most of their code, use `pytest.mark.parametrize` to merge them into a single parameterized test instead of duplicating the body. Use `pytest.param` with an `id` parameter to name the test cases clearly.
|
||||||
- We use Syrupy for snapshot testing. Leverage `.ambr` snapshots instead of repetitive and exhaustive generation of test data within Python code itself.
|
- We use Syrupy for snapshot testing. Leverage `.ambr` snapshots instead of repetitive and exhaustive generation of test data within Python code itself.
|
||||||
- Hardcoded `entity_id`s in tests are fine. If the same one is repeated, use a constant.
|
|
||||||
|
|
||||||
## Good practices
|
## Good practices
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -2054,8 +2054,8 @@ CLAUDE.md @home-assistant/core
|
|||||||
/tests/components/yamaha_musiccast/ @vigonotion @micha91
|
/tests/components/yamaha_musiccast/ @vigonotion @micha91
|
||||||
/homeassistant/components/yandex_transport/ @rishatik92 @devbis
|
/homeassistant/components/yandex_transport/ @rishatik92 @devbis
|
||||||
/tests/components/yandex_transport/ @rishatik92 @devbis
|
/tests/components/yandex_transport/ @rishatik92 @devbis
|
||||||
/homeassistant/components/yardian/ @aeon-matrix
|
/homeassistant/components/yardian/ @h3l1o5
|
||||||
/tests/components/yardian/ @aeon-matrix
|
/tests/components/yardian/ @h3l1o5
|
||||||
/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||||
/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||||
/homeassistant/components/yeelightsunflower/ @lindsaymarkward
|
/homeassistant/components/yeelightsunflower/ @lindsaymarkward
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ def _extract_backup(
|
|||||||
):
|
):
|
||||||
ostf.tar.extractall(
|
ostf.tar.extractall(
|
||||||
path=Path(tempdir, "extracted"),
|
path=Path(tempdir, "extracted"),
|
||||||
filter="tar",
|
members=securetar.secure_path(ostf.tar),
|
||||||
|
filter="fully_trusted",
|
||||||
)
|
)
|
||||||
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
backup_meta_file = Path(tempdir, "extracted", "backup.json")
|
||||||
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
|
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
|
||||||
@@ -118,7 +119,8 @@ def _extract_backup(
|
|||||||
) as istf:
|
) as istf:
|
||||||
istf.extractall(
|
istf.extractall(
|
||||||
path=Path(tempdir, "homeassistant"),
|
path=Path(tempdir, "homeassistant"),
|
||||||
filter="tar",
|
members=securetar.secure_path(istf),
|
||||||
|
filter="fully_trusted",
|
||||||
)
|
)
|
||||||
if restore_content.restore_homeassistant:
|
if restore_content.restore_homeassistant:
|
||||||
keep = list(KEEP_BACKUPS)
|
keep = list(KEEP_BACKUPS)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"lg_netcast",
|
"lg_netcast",
|
||||||
"lg_soundbar",
|
"lg_soundbar",
|
||||||
"lg_thinq",
|
"lg_thinq",
|
||||||
"lg_tv_rs232",
|
|
||||||
"webostv"
|
"webostv"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from homeassistant.components.select import (
|
|||||||
PLATFORM_SCHEMA as SELECT_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as SELECT_PLATFORM_SCHEMA,
|
||||||
SelectEntity,
|
SelectEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_NAME, CONF_OPTIONS
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@@ -19,6 +19,9 @@ from .hub import AdsHub
|
|||||||
|
|
||||||
DEFAULT_NAME = "ADS select"
|
DEFAULT_NAME = "ADS select"
|
||||||
|
|
||||||
|
# pylint: disable-next=home-assistant-duplicate-const
|
||||||
|
CONF_OPTIONS = "options"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = SELECT_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = SELECT_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields:
|
.trigger_common_fields:
|
||||||
behavior: &trigger_behavior
|
behavior: &trigger_behavior
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields: &trigger_common_fields
|
fields: &trigger_common_fields
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
|
|||||||
|
|
||||||
_attr_event_types = [EVENT_TYPE]
|
_attr_event_types = [EVENT_TYPE]
|
||||||
coordinator: AmazonDevicesCoordinator
|
coordinator: AmazonDevicesCoordinator
|
||||||
_last_seen_timestamp: int = 0 # January 1, 1970 at 12:00:00 AM
|
_last_seen_timestamp: int | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
@@ -71,8 +71,7 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if vocal_record.timestamp <= self._last_seen_timestamp:
|
if vocal_record.timestamp == self._last_seen_timestamp:
|
||||||
# Discard old events that have already been processed
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self._last_seen_timestamp = vocal_record.timestamp
|
self._last_seen_timestamp = vocal_record.timestamp
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioamazondevices==13.8.1"]
|
"requirements": ["aioamazondevices==13.8.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,13 +38,11 @@ from homeassistant.components.media_player import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import AppleTvConfigEntry, AppleTVManager
|
from . import AppleTvConfigEntry, AppleTVManager
|
||||||
from .browse_media import build_app_list
|
from .browse_media import build_app_list
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import AppleTVEntity
|
from .entity import AppleTVEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -128,6 +126,7 @@ class AppleTvMediaPlayer(
|
|||||||
@callback
|
@callback
|
||||||
def async_device_connected(self, atv: AppleTV) -> None:
|
def async_device_connected(self, atv: AppleTV) -> None:
|
||||||
"""Handle when connection is made to device."""
|
"""Handle when connection is made to device."""
|
||||||
|
# NB: Do not use _is_feature_available here as it only works when playing
|
||||||
if atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates):
|
if atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates):
|
||||||
atv.push_updater.listener = self
|
atv.push_updater.listener = self
|
||||||
atv.push_updater.start()
|
atv.push_updater.start()
|
||||||
@@ -353,41 +352,21 @@ class AppleTvMediaPlayer(
|
|||||||
media_id = async_process_play_media_url(self.hass, play_item.url)
|
media_id = async_process_play_media_url(self.hass, play_item.url)
|
||||||
media_type = MediaType.MUSIC
|
media_type = MediaType.MUSIC
|
||||||
|
|
||||||
use_stream_file = self._is_feature_available(FeatureName.StreamFile) and (
|
if self._is_feature_available(FeatureName.StreamFile) and (
|
||||||
media_type == MediaType.MUSIC or await is_streamable(media_id)
|
media_type == MediaType.MUSIC or await is_streamable(media_id)
|
||||||
)
|
):
|
||||||
|
_LOGGER.debug("Streaming %s via RAOP", media_id)
|
||||||
try:
|
await self.atv.stream.stream_file(media_id)
|
||||||
if use_stream_file:
|
elif self._is_feature_available(FeatureName.PlayUrl) and (
|
||||||
_LOGGER.debug("Streaming %s via RAOP", media_id)
|
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
|
||||||
await self.atv.stream.stream_file(media_id)
|
):
|
||||||
elif self._is_feature_available(FeatureName.PlayUrl) and (
|
_LOGGER.debug("Playing %s via AirPlay", media_id)
|
||||||
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
|
await self.atv.stream.play_url(media_id)
|
||||||
):
|
else:
|
||||||
_LOGGER.debug("Playing %s via AirPlay", media_id)
|
_LOGGER.error(
|
||||||
await self.atv.stream.play_url(media_id)
|
"Media streaming is not possible with current configuration for %s",
|
||||||
else:
|
media_id,
|
||||||
raise HomeAssistantError(
|
)
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="streaming_not_supported",
|
|
||||||
)
|
|
||||||
except exceptions.NotSupportedError as ex:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="streaming_not_supported",
|
|
||||||
) from ex
|
|
||||||
except (
|
|
||||||
exceptions.BlockedStateError,
|
|
||||||
exceptions.ConnectionLostError,
|
|
||||||
exceptions.InvalidStateError,
|
|
||||||
exceptions.OperationTimeoutError,
|
|
||||||
exceptions.PlaybackError,
|
|
||||||
exceptions.ProtocolError,
|
|
||||||
) as ex:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="stream_failed",
|
|
||||||
) from ex
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_hash(self) -> str | None:
|
def media_image_hash(self) -> str | None:
|
||||||
@@ -481,7 +460,7 @@ class AppleTvMediaPlayer(
|
|||||||
|
|
||||||
def _is_feature_available(self, feature: FeatureName) -> bool:
|
def _is_feature_available(self, feature: FeatureName) -> bool:
|
||||||
"""Return if a feature is available."""
|
"""Return if a feature is available."""
|
||||||
if self.atv:
|
if self.atv and self._playing:
|
||||||
return self.atv.features.in_state(FeatureState.Available, feature)
|
return self.atv.features.in_state(FeatureState.Available, feature)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -81,12 +81,6 @@
|
|||||||
},
|
},
|
||||||
"not_connected": {
|
"not_connected": {
|
||||||
"message": "Apple TV is not connected"
|
"message": "Apple TV is not connected"
|
||||||
},
|
|
||||||
"stream_failed": {
|
|
||||||
"message": "Failed to stream media to the Apple TV"
|
|
||||||
},
|
|
||||||
"streaming_not_supported": {
|
|
||||||
"message": "Streaming the requested media is not supported"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -30,5 +30,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
|
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.3.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
|||||||
translation_key="invalid_bucket_name",
|
translation_key="invalid_bucket_name",
|
||||||
) from err
|
) from err
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
|
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||||
raise ConfigEntryError(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_endpoint_url",
|
translation_key="invalid_endpoint_url",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]",
|
"cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]",
|
||||||
"invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]",
|
"invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]",
|
||||||
"invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]",
|
"invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]",
|
||||||
"invalid_endpoint_url": "[%key:component::aws_s3::exceptions::invalid_endpoint_url::message%]"
|
"invalid_endpoint_url": "Invalid endpoint URL. Please make sure it's a valid AWS S3 endpoint URL."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
@@ -48,9 +48,6 @@
|
|||||||
},
|
},
|
||||||
"invalid_credentials": {
|
"invalid_credentials": {
|
||||||
"message": "Bucket cannot be accessed using provided combination of access key ID and secret access key."
|
"message": "Bucket cannot be accessed using provided combination of access key ID and secret access key."
|
||||||
},
|
|
||||||
"invalid_endpoint_url": {
|
|
||||||
"message": "Invalid endpoint URL. Please make sure it's a valid AWS S3 endpoint URL."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from homeassistant.helpers.hassio import is_hassio
|
|||||||
|
|
||||||
from .agent import BackupAgent, LocalBackupAgent, OnProgressCallback
|
from .agent import BackupAgent, LocalBackupAgent, OnProgressCallback
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
from .models import AgentBackup, BackupNotFound, InvalidBackupFilename
|
from .models import AgentBackup, BackupNotFound
|
||||||
from .util import read_backup, suggested_filename
|
from .util import read_backup, suggested_filename
|
||||||
|
|
||||||
|
|
||||||
@@ -54,13 +54,7 @@ class CoreLocalBackupAgent(LocalBackupAgent):
|
|||||||
try:
|
try:
|
||||||
backup = read_backup(backup_path)
|
backup = read_backup(backup_path)
|
||||||
backups[backup.backup_id] = (backup, backup_path)
|
backups[backup.backup_id] = (backup, backup_path)
|
||||||
except (
|
except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
|
||||||
OSError,
|
|
||||||
TarError,
|
|
||||||
json.JSONDecodeError,
|
|
||||||
KeyError,
|
|
||||||
InvalidBackupFilename,
|
|
||||||
) as err:
|
|
||||||
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
|
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
|
||||||
return backups
|
return backups
|
||||||
|
|
||||||
@@ -128,14 +122,7 @@ class CoreLocalBackupAgent(LocalBackupAgent):
|
|||||||
|
|
||||||
def get_new_backup_path(self, backup: AgentBackup) -> Path:
|
def get_new_backup_path(self, backup: AgentBackup) -> Path:
|
||||||
"""Return the local path to a new backup."""
|
"""Return the local path to a new backup."""
|
||||||
candidate = self._backup_dir / suggested_filename(backup)
|
return self._backup_dir / suggested_filename(backup)
|
||||||
# suggested_filename does not strip separators; refuse paths that would
|
|
||||||
# land outside the backup directory.
|
|
||||||
if candidate.parent != self._backup_dir:
|
|
||||||
raise InvalidBackupFilename(
|
|
||||||
f"Refusing to write outside {self._backup_dir}: {candidate}"
|
|
||||||
)
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
|
async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
|
||||||
"""Delete a backup file."""
|
"""Delete a backup file."""
|
||||||
|
|||||||
@@ -1978,13 +1978,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
backup = await async_add_executor_job(read_backup, temp_file)
|
backup = await async_add_executor_job(read_backup, temp_file)
|
||||||
except (
|
except (OSError, tarfile.TarError, json.JSONDecodeError, KeyError) as err:
|
||||||
OSError,
|
|
||||||
tarfile.TarError,
|
|
||||||
json.JSONDecodeError,
|
|
||||||
KeyError,
|
|
||||||
InvalidBackupFilename,
|
|
||||||
) as err:
|
|
||||||
LOGGER.warning("Unable to parse backup %s: %s", temp_file, err)
|
LOGGER.warning("Unable to parse backup %s: %s", temp_file, err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import copy
|
|||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import json
|
import json
|
||||||
from pathlib import Path, PurePath, PureWindowsPath
|
from pathlib import Path, PurePath
|
||||||
from queue import SimpleQueue
|
from queue import SimpleQueue
|
||||||
import tarfile
|
import tarfile
|
||||||
import threading
|
import threading
|
||||||
@@ -34,7 +34,7 @@ from homeassistant.util.async_iterator import (
|
|||||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||||
|
|
||||||
from .const import BUF_SIZE, LOGGER, SECURETAR_CREATE_VERSION
|
from .const import BUF_SIZE, LOGGER, SECURETAR_CREATE_VERSION
|
||||||
from .models import AddonInfo, AgentBackup, Folder, InvalidBackupFilename
|
from .models import AddonInfo, AgentBackup, Folder
|
||||||
|
|
||||||
|
|
||||||
class DecryptError(HomeAssistantError):
|
class DecryptError(HomeAssistantError):
|
||||||
@@ -109,13 +109,6 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
|||||||
extra_metadata = cast(dict[str, bool | str], data.get("extra", {}))
|
extra_metadata = cast(dict[str, bool | str], data.get("extra", {}))
|
||||||
date = extra_metadata.get("supervisor.backup_request_date", data["date"])
|
date = extra_metadata.get("supervisor.backup_request_date", data["date"])
|
||||||
|
|
||||||
name = cast(str, data["name"])
|
|
||||||
# The name is used to derive the on-disk filename via suggested_filename;
|
|
||||||
# reject anything that could escape the backup directory.
|
|
||||||
safe_name = PureWindowsPath(name).name
|
|
||||||
if safe_name != name or name in ("", ".", ".."):
|
|
||||||
raise InvalidBackupFilename(f"Invalid backup name: {name!r}")
|
|
||||||
|
|
||||||
return AgentBackup(
|
return AgentBackup(
|
||||||
addons=addons,
|
addons=addons,
|
||||||
backup_id=cast(str, data["slug"]),
|
backup_id=cast(str, data["slug"]),
|
||||||
@@ -125,7 +118,7 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
|||||||
folders=folders,
|
folders=folders,
|
||||||
homeassistant_included=homeassistant_included,
|
homeassistant_included=homeassistant_included,
|
||||||
homeassistant_version=homeassistant_version,
|
homeassistant_version=homeassistant_version,
|
||||||
name=name,
|
name=cast(str, data["name"]),
|
||||||
protected=cast(bool, data.get("protected", False)),
|
protected=cast(bool, data.get("protected", False)),
|
||||||
size=backup_path.stat().st_size,
|
size=backup_path.stat().st_size,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields:
|
.trigger_common_fields:
|
||||||
behavior: &trigger_behavior
|
behavior: &trigger_behavior
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"bluetooth-adapters==2.3.0",
|
"bluetooth-adapters==2.3.0",
|
||||||
"bluetooth-auto-recovery==1.6.4",
|
"bluetooth-auto-recovery==1.6.4",
|
||||||
"bluetooth-data-tools==1.29.18",
|
"bluetooth-data-tools==1.29.18",
|
||||||
"dbus-fast==5.0.16",
|
"dbus-fast==5.0.14",
|
||||||
"habluetooth==6.7.9"
|
"habluetooth==6.7.4"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class BroadlinkRadioFrequency(BroadlinkEntity, RadioFrequencyTransmitterEntity):
|
|||||||
"""Representation of a Broadlink RF transmitter."""
|
"""Representation of a Broadlink RF transmitter."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_translation_key = "rf_transmitter"
|
_attr_name = None
|
||||||
|
|
||||||
def __init__(self, device: BroadlinkDevice) -> None:
|
def __init__(self, device: BroadlinkDevice) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
|
|||||||
@@ -54,11 +54,6 @@
|
|||||||
"name": "IR emitter"
|
"name": "IR emitter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"radio_frequency": {
|
|
||||||
"rf_transmitter": {
|
|
||||||
"name": "RF transmitter"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"select": {
|
"select": {
|
||||||
"day_of_week": {
|
"day_of_week": {
|
||||||
"name": "Day of week",
|
"name": "Day of week",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant, State
|
|||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.automation import DomainSpec
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import (
|
||||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR,
|
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||||
EntityNumericalStateChangedTriggerBase,
|
EntityNumericalStateChangedTriggerBase,
|
||||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||||
EntityNumericalStateCrossedThresholdTriggerBase,
|
EntityNumericalStateCrossedThresholdTriggerBase,
|
||||||
@@ -26,7 +26,7 @@ from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
|||||||
|
|
||||||
CONF_HVAC_MODE = "hvac_mode"
|
CONF_HVAC_MODE = "hvac_mode"
|
||||||
|
|
||||||
HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR.extend(
|
HVAC_MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_OPTIONS): {
|
vol.Required(CONF_OPTIONS): {
|
||||||
vol.Required(CONF_HVAC_MODE): vol.All(
|
vol.Required(CONF_HVAC_MODE): vol.All(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
behavior: &trigger_behavior
|
behavior: &trigger_behavior
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ class ConfigManagerFlowIndexView(
|
|||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required("handler"): vol.Any(str, list),
|
vol.Required("handler"): vol.Any(str, list),
|
||||||
|
vol.Optional("show_advanced_options", default=False): cv.boolean,
|
||||||
vol.Optional("entry_id"): cv.string,
|
vol.Optional("entry_id"): cv.string,
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
@@ -301,6 +302,7 @@ class SubentryManagerFlowIndexView(
|
|||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required("handler"): vol.All(vol.Coerce(tuple), (str, str)),
|
vol.Required("handler"): vol.All(vol.Coerce(tuple), (str, str)),
|
||||||
|
vol.Optional("show_advanced_options", default=False): cv.boolean,
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields: &trigger_common_fields
|
.trigger_common_fields: &trigger_common_fields
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from .const import ( # noqa: F401
|
|||||||
ATTR_LOCATION_NAME,
|
ATTR_LOCATION_NAME,
|
||||||
ATTR_MAC,
|
ATTR_MAC,
|
||||||
ATTR_SOURCE_TYPE,
|
ATTR_SOURCE_TYPE,
|
||||||
CONF_ASSOCIATED_ZONE,
|
|
||||||
CONF_CONSIDER_HOME,
|
CONF_CONSIDER_HOME,
|
||||||
CONF_NEW_DEVICE_DEFAULTS,
|
CONF_NEW_DEVICE_DEFAULTS,
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ DEFAULT_CONSIDER_HOME: Final = timedelta(seconds=180)
|
|||||||
|
|
||||||
CONF_NEW_DEVICE_DEFAULTS: Final = "new_device_defaults"
|
CONF_NEW_DEVICE_DEFAULTS: Final = "new_device_defaults"
|
||||||
|
|
||||||
CONF_ASSOCIATED_ZONE: Final = "associated_zone"
|
|
||||||
|
|
||||||
ATTR_ATTRIBUTES: Final = "attributes"
|
ATTR_ATTRIBUTES: Final = "attributes"
|
||||||
ATTR_BATTERY: Final = "battery"
|
ATTR_BATTERY: Final = "battery"
|
||||||
ATTR_DEV_ID: Final = "dev_id"
|
ATTR_DEV_ID: Final = "dev_id"
|
||||||
|
|||||||
@@ -12,19 +12,13 @@ from homeassistant.const import (
|
|||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
CONF_OPTIONS,
|
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
CONF_ZONE,
|
CONF_ZONE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||||
TriggerActionType,
|
|
||||||
TriggerInfo,
|
|
||||||
# protected, but only used for legacy triggers
|
|
||||||
_async_attach_trigger_cls,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -85,18 +79,16 @@ async def async_attach_trigger(
|
|||||||
event = zone.EVENT_ENTER
|
event = zone.EVENT_ENTER
|
||||||
else:
|
else:
|
||||||
event = zone.EVENT_LEAVE
|
event = zone.EVENT_LEAVE
|
||||||
zone_config = await zone.LegacyZoneTrigger.async_validate_config(
|
|
||||||
hass,
|
zone_config = {
|
||||||
{
|
CONF_PLATFORM: ZONE_DOMAIN,
|
||||||
CONF_OPTIONS: {
|
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||||
CONF_ENTITY_ID: [config[CONF_ENTITY_ID]],
|
CONF_ZONE: config[CONF_ZONE],
|
||||||
CONF_ZONE: config[CONF_ZONE],
|
CONF_EVENT: event,
|
||||||
CONF_EVENT: event,
|
}
|
||||||
}
|
zone_config = await zone.async_validate_trigger_config(hass, zone_config)
|
||||||
},
|
return await zone.async_attach_trigger(
|
||||||
)
|
hass, zone_config, action, trigger_info, platform_type="device"
|
||||||
return await _async_attach_trigger_cls(
|
|
||||||
hass, zone.LegacyZoneTrigger, "device", zone_config, action, trigger_info
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"""Provide functionality to keep track of devices."""
|
"""Provide functionality to keep track of devices."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
from typing import Any, final
|
||||||
from typing import TYPE_CHECKING, Any, final
|
|
||||||
|
|
||||||
from propcache.api import cached_property
|
from propcache.api import cached_property
|
||||||
|
|
||||||
@@ -17,20 +16,8 @@ from homeassistant.const import (
|
|||||||
STATE_NOT_HOME,
|
STATE_NOT_HOME,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
)
|
)
|
||||||
from homeassistant.core import (
|
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||||
CALLBACK_TYPE,
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
Event,
|
|
||||||
EventStateChangedData,
|
|
||||||
HomeAssistant,
|
|
||||||
State,
|
|
||||||
async_get_hass_or_none,
|
|
||||||
callback,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers import (
|
|
||||||
device_registry as dr,
|
|
||||||
entity_registry as er,
|
|
||||||
issue_registry as ir,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.device_registry import (
|
from homeassistant.helpers.device_registry import (
|
||||||
DeviceInfo,
|
DeviceInfo,
|
||||||
EventDeviceRegistryUpdatedData,
|
EventDeviceRegistryUpdatedData,
|
||||||
@@ -38,8 +25,6 @@ from homeassistant.helpers.device_registry import (
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
from homeassistant.helpers.event import async_track_state_change_event
|
|
||||||
from homeassistant.loader import async_suggest_report_issue
|
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@@ -48,15 +33,12 @@ from .const import (
|
|||||||
ATTR_IP,
|
ATTR_IP,
|
||||||
ATTR_MAC,
|
ATTR_MAC,
|
||||||
ATTR_SOURCE_TYPE,
|
ATTR_SOURCE_TYPE,
|
||||||
CONF_ASSOCIATED_ZONE,
|
|
||||||
CONNECTED_DEVICE_REGISTERED,
|
CONNECTED_DEVICE_REGISTERED,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
SourceType,
|
SourceType,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
DATA_KEY: HassKey[dict[str, tuple[str, str]]] = HassKey(f"{DOMAIN}_mac")
|
DATA_KEY: HassKey[dict[str, tuple[str, str]]] = HassKey(f"{DOMAIN}_mac")
|
||||||
|
|
||||||
|
|
||||||
@@ -169,35 +151,11 @@ class BaseTrackerEntity(Entity):
|
|||||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
_attr_source_type: SourceType
|
_attr_source_type: SourceType
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
||||||
"""Post initialisation processing."""
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
if "battery_level" in cls.__dict__:
|
|
||||||
if cls.__module__.startswith("homeassistant.components."):
|
|
||||||
# Don't ask users to report issue for built in integrations,
|
|
||||||
# they already have issues opened on them.
|
|
||||||
return
|
|
||||||
report_issue = async_suggest_report_issue(
|
|
||||||
async_get_hass_or_none(), module=cls.__module__
|
|
||||||
)
|
|
||||||
_LOGGER.warning(
|
|
||||||
(
|
|
||||||
"%s::%s is overriding the deprecated battery_level property on "
|
|
||||||
"a subclass of BaseTrackerEntity, this will be unsupported from "
|
|
||||||
"Home Assistant 2027.7, please %s"
|
|
||||||
),
|
|
||||||
cls.__module__,
|
|
||||||
cls.__name__,
|
|
||||||
report_issue,
|
|
||||||
)
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def battery_level(self) -> int | None:
|
def battery_level(self) -> int | None:
|
||||||
"""Return the battery level of the device.
|
"""Return the battery level of the device.
|
||||||
|
|
||||||
Percentage from 0-100.
|
Percentage from 0-100.
|
||||||
|
|
||||||
The property is deprecated and will be removed in Home Assistant 2027.7.
|
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -241,38 +199,13 @@ class TrackerEntity(
|
|||||||
_attr_in_zones: list[str] | None = None
|
_attr_in_zones: list[str] | None = None
|
||||||
_attr_latitude: float | None = None
|
_attr_latitude: float | None = None
|
||||||
_attr_location_accuracy: float = 0
|
_attr_location_accuracy: float = 0
|
||||||
# _attr_location_name is deprecated and will be removed in Home Assistant 2027.7
|
|
||||||
_attr_location_name: str | None = None
|
_attr_location_name: str | None = None
|
||||||
_attr_longitude: float | None = None
|
_attr_longitude: float | None = None
|
||||||
_attr_source_type: SourceType = SourceType.GPS
|
_attr_source_type: SourceType = SourceType.GPS
|
||||||
|
|
||||||
__active_zone: State | None = None
|
__active_zone: State | None = None
|
||||||
# If we reported setting deprecated _attr_location_name
|
|
||||||
__deprecated_attr_location_name_reported = False
|
|
||||||
__in_zones: list[str] | None = None
|
__in_zones: list[str] | None = None
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
||||||
"""Post initialisation processing."""
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
if "location_name" in cls.__dict__:
|
|
||||||
if cls.__module__.startswith("homeassistant.components."):
|
|
||||||
# Don't ask users to report issue for built in integrations,
|
|
||||||
# they already have issues opened on them.
|
|
||||||
return
|
|
||||||
report_issue = async_suggest_report_issue(
|
|
||||||
async_get_hass_or_none(), module=cls.__module__
|
|
||||||
)
|
|
||||||
_LOGGER.warning(
|
|
||||||
(
|
|
||||||
"%s::%s is overriding the deprecated location_name property on "
|
|
||||||
"an instance of TrackerEntity, this will be unsupported from "
|
|
||||||
"Home Assistant 2027.7, please %s"
|
|
||||||
),
|
|
||||||
cls.__module__,
|
|
||||||
cls.__name__,
|
|
||||||
report_issue,
|
|
||||||
)
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def should_poll(self) -> bool:
|
def should_poll(self) -> bool:
|
||||||
"""No polling for entities that have location pushed."""
|
"""No polling for entities that have location pushed."""
|
||||||
@@ -288,8 +221,8 @@ class TrackerEntity(
|
|||||||
"""Return the entity_id of zones the device is currently in.
|
"""Return the entity_id of zones the device is currently in.
|
||||||
|
|
||||||
The list may be in any order; the base class sorts it by zone radius
|
The list may be in any order; the base class sorts it by zone radius
|
||||||
and discards zones which do not exist. Takes precedence over latitude
|
and discards zones which do not exist. Ignored if latitude and
|
||||||
and longitude when set (including when set to an empty list).
|
longitude are both set.
|
||||||
"""
|
"""
|
||||||
return self._attr_in_zones
|
return self._attr_in_zones
|
||||||
|
|
||||||
@@ -303,32 +236,7 @@ class TrackerEntity(
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def location_name(self) -> str | None:
|
def location_name(self) -> str | None:
|
||||||
"""Return a location name for the current location of the device.
|
"""Return a location name for the current location of the device."""
|
||||||
|
|
||||||
The property is deprecated and will be removed in Home Assistant 2027.7.
|
|
||||||
"""
|
|
||||||
if (location_name := self._attr_location_name) is not None:
|
|
||||||
if (
|
|
||||||
not self.__deprecated_attr_location_name_reported
|
|
||||||
and not self.__class__.__module__.startswith(
|
|
||||||
"homeassistant.components."
|
|
||||||
)
|
|
||||||
):
|
|
||||||
report_issue = async_suggest_report_issue(
|
|
||||||
self.hass, module=self.__class__.__module__
|
|
||||||
)
|
|
||||||
_LOGGER.warning(
|
|
||||||
(
|
|
||||||
"%s::%s is setting the deprecated _attr_location_name attribute "
|
|
||||||
"on an instance of TrackerEntity, this will be unsupported from "
|
|
||||||
"Home Assistant 2027.7, please %s"
|
|
||||||
),
|
|
||||||
self.__class__.__module__,
|
|
||||||
self.__class__.__name__,
|
|
||||||
report_issue,
|
|
||||||
)
|
|
||||||
self.__deprecated_attr_location_name_reported = True
|
|
||||||
return location_name
|
|
||||||
return self._attr_location_name
|
return self._attr_location_name
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@@ -344,7 +252,11 @@ class TrackerEntity(
|
|||||||
@callback
|
@callback
|
||||||
def _async_write_ha_state(self) -> None:
|
def _async_write_ha_state(self) -> None:
|
||||||
"""Calculate active zones."""
|
"""Calculate active zones."""
|
||||||
if (zones := self.in_zones) is not None:
|
if self.available and self.latitude is not None and self.longitude is not None:
|
||||||
|
self.__active_zone, self.__in_zones = zone.async_in_zones(
|
||||||
|
self.hass, self.latitude, self.longitude, self.location_accuracy
|
||||||
|
)
|
||||||
|
elif (zones := self.in_zones) is not None:
|
||||||
zone_states = sorted(
|
zone_states = sorted(
|
||||||
(
|
(
|
||||||
zone_state
|
zone_state
|
||||||
@@ -358,12 +270,6 @@ class TrackerEntity(
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
self.__in_zones = [z.entity_id for z in zone_states]
|
self.__in_zones = [z.entity_id for z in zone_states]
|
||||||
elif (
|
|
||||||
self.available and self.latitude is not None and self.longitude is not None
|
|
||||||
):
|
|
||||||
self.__active_zone, self.__in_zones = zone.async_in_zones(
|
|
||||||
self.hass, self.latitude, self.longitude, self.location_accuracy
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.__active_zone = None
|
self.__active_zone = None
|
||||||
self.__in_zones = None
|
self.__in_zones = None
|
||||||
@@ -411,120 +317,14 @@ class BaseScannerEntity(BaseTrackerEntity):
|
|||||||
addresses being used to identify the device.
|
addresses being used to identify the device.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_scanner_option_associated_zone: str = zone.ENTITY_ID_HOME
|
|
||||||
_scanner_option_associated_zone_unsub: CALLBACK_TYPE | None = None
|
|
||||||
|
|
||||||
async def async_internal_added_to_hass(self) -> None:
|
|
||||||
"""Call when the scanner entity is added to hass."""
|
|
||||||
await super().async_internal_added_to_hass()
|
|
||||||
if not self.registry_entry:
|
|
||||||
return
|
|
||||||
self._async_read_entity_options()
|
|
||||||
|
|
||||||
async def async_internal_will_remove_from_hass(self) -> None:
|
|
||||||
"""Call when the scanner entity is about to be removed from hass."""
|
|
||||||
await super().async_internal_will_remove_from_hass()
|
|
||||||
if not self.registry_entry:
|
|
||||||
return
|
|
||||||
if self._scanner_option_associated_zone_unsub is not None:
|
|
||||||
self._scanner_option_associated_zone_unsub()
|
|
||||||
self._scanner_option_associated_zone_unsub = None
|
|
||||||
self._async_clear_associated_zone_issue()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_registry_entry_updated(self) -> None:
|
|
||||||
"""Run when the entity registry entry has been updated."""
|
|
||||||
self._async_read_entity_options()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_read_entity_options(self) -> None:
|
|
||||||
"""Read entity options from the entity registry.
|
|
||||||
|
|
||||||
Called when the entity registry entry has been updated and before the
|
|
||||||
scanner entity is added to the state machine.
|
|
||||||
"""
|
|
||||||
assert self.registry_entry
|
|
||||||
if (scanner_options := self.registry_entry.options.get(DOMAIN)) and (
|
|
||||||
associated_zone := scanner_options.get(CONF_ASSOCIATED_ZONE)
|
|
||||||
):
|
|
||||||
new_zone = associated_zone
|
|
||||||
else:
|
|
||||||
new_zone = zone.ENTITY_ID_HOME
|
|
||||||
|
|
||||||
if new_zone == self._scanner_option_associated_zone:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Tear down tracking for the previous zone.
|
|
||||||
if self._scanner_option_associated_zone_unsub is not None:
|
|
||||||
self._scanner_option_associated_zone_unsub()
|
|
||||||
self._scanner_option_associated_zone_unsub = None
|
|
||||||
self._async_clear_associated_zone_issue()
|
|
||||||
|
|
||||||
self._scanner_option_associated_zone = new_zone
|
|
||||||
|
|
||||||
# zone.home is always present so no tracking or issue handling needed.
|
|
||||||
if new_zone == zone.ENTITY_ID_HOME:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._scanner_option_associated_zone_unsub = async_track_state_change_event(
|
|
||||||
self.hass, new_zone, self._async_associated_zone_state_changed
|
|
||||||
)
|
|
||||||
if self.hass.states.get(new_zone) is None:
|
|
||||||
self._async_create_associated_zone_issue()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_associated_zone_state_changed(
|
|
||||||
self, event: Event[EventStateChangedData]
|
|
||||||
) -> None:
|
|
||||||
"""Open or clear the repair issue when the associated zone appears or disappears."""
|
|
||||||
if event.data["new_state"] is None:
|
|
||||||
self._async_create_associated_zone_issue()
|
|
||||||
else:
|
|
||||||
self._async_clear_associated_zone_issue()
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_create_associated_zone_issue(self) -> None:
|
|
||||||
"""Create a repair issue prompting the user to reconfigure the scanner."""
|
|
||||||
ir.async_create_issue(
|
|
||||||
self.hass,
|
|
||||||
DOMAIN,
|
|
||||||
self._associated_zone_issue_id,
|
|
||||||
is_fixable=False,
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key="associated_zone_missing",
|
|
||||||
translation_placeholders={
|
|
||||||
"entity_id": self.entity_id,
|
|
||||||
"zone": self._scanner_option_associated_zone,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_clear_associated_zone_issue(self) -> None:
|
|
||||||
"""Clear the associated-zone-missing repair issue if it exists."""
|
|
||||||
ir.async_delete_issue(self.hass, DOMAIN, self._associated_zone_issue_id)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _associated_zone_issue_id(self) -> str:
|
|
||||||
"""Return the issue id for the associated-zone-missing repair."""
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert self.registry_entry
|
|
||||||
return f"associated_zone_missing_{self.registry_entry.id}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def state(self) -> str | None:
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self.is_connected is None:
|
if self.is_connected is None:
|
||||||
return None
|
return None
|
||||||
if not self.is_connected:
|
if self.is_connected:
|
||||||
return STATE_NOT_HOME
|
|
||||||
associated_zone = self._scanner_option_associated_zone
|
|
||||||
if associated_zone == zone.ENTITY_ID_HOME:
|
|
||||||
return STATE_HOME
|
return STATE_HOME
|
||||||
if zone_state := self.hass.states.get(associated_zone):
|
return STATE_NOT_HOME
|
||||||
return zone_state.name
|
|
||||||
# Configured zone has been removed; state is unknown.
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self) -> bool | None:
|
def is_connected(self) -> bool | None:
|
||||||
@@ -541,18 +341,9 @@ class BaseScannerEntity(BaseTrackerEntity):
|
|||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
associated_zone = self._scanner_option_associated_zone
|
|
||||||
# If the configured zone has been removed, in_zones stays empty so the
|
|
||||||
# attribute does not claim membership in a zone that no longer exists.
|
|
||||||
if (
|
|
||||||
associated_zone != zone.ENTITY_ID_HOME
|
|
||||||
and self.hass.states.get(associated_zone) is None
|
|
||||||
):
|
|
||||||
return attr
|
|
||||||
|
|
||||||
attr[ATTR_IN_ZONES] = [
|
attr[ATTR_IN_ZONES] = [
|
||||||
associated_zone,
|
zone.ENTITY_ID_HOME,
|
||||||
*zone.async_get_enclosing_zones(self.hass, associated_zone),
|
*zone.async_get_enclosing_zones(self.hass, zone.ENTITY_ID_HOME),
|
||||||
]
|
]
|
||||||
|
|
||||||
return attr
|
return attr
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import (
|
|
||||||
async_create_platform_config_not_supported_issue,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_time_interval,
|
async_track_time_interval,
|
||||||
async_track_utc_time_change,
|
async_track_utc_time_change,
|
||||||
@@ -382,8 +379,8 @@ async def async_extract_config(
|
|||||||
if platform.type == PLATFORM_TYPE_LEGACY:
|
if platform.type == PLATFORM_TYPE_LEGACY:
|
||||||
legacy.append(platform)
|
legacy.append(platform)
|
||||||
else:
|
else:
|
||||||
async_create_platform_config_not_supported_issue(
|
raise ValueError(
|
||||||
hass, platform.name, DOMAIN
|
f"Unable to determine type for {platform.name}: {platform.type}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return legacy
|
return legacy
|
||||||
|
|||||||
@@ -44,12 +44,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
|
||||||
"associated_zone_missing": {
|
|
||||||
"description": "The scanner entity `{entity_id}` is associated with the zone `{zone}`, but that zone has been removed.\n\nTo fix this, reconfigure the scanner to use a different zone or recreate the missing zone.",
|
|
||||||
"title": "Scanner is associated with a removed zone"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"see": {
|
"see": {
|
||||||
"description": "Manually update the records of a seen legacy device tracker in the known_devices.yaml file.",
|
"description": "Manually update the records of a seen legacy device tracker in the known_devices.yaml file.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields: &trigger_common_fields
|
.trigger_common_fields: &trigger_common_fields
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class DuckDnsUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
"""Update Duck DNS."""
|
"""Update Duck DNS."""
|
||||||
|
|
||||||
retry_after = BACKOFF_INTERVALS[
|
retry_after = BACKOFF_INTERVALS[
|
||||||
min(self.failed, len(BACKOFF_INTERVALS) - 1)
|
min(self.failed, len(BACKOFF_INTERVALS))
|
||||||
].total_seconds()
|
].total_seconds()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
|||||||
"""Fetch node data from the Duco box."""
|
"""Fetch node data from the Duco box."""
|
||||||
try:
|
try:
|
||||||
nodes = await self.client.async_get_nodes()
|
nodes = await self.client.async_get_nodes()
|
||||||
|
lan_info = await self.client.async_get_lan_info()
|
||||||
except DucoConnectionError as err:
|
except DucoConnectionError as err:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
@@ -99,18 +100,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
|||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
# LAN info only backs the diagnostic RSSI sensor, so failures on this
|
|
||||||
# supplemental endpoint, including connection failures, should not make
|
|
||||||
# the primary node entities unavailable.
|
|
||||||
rssi_wifi = self.data.rssi_wifi if self.data else None
|
|
||||||
try:
|
|
||||||
lan_info = await self.client.async_get_lan_info()
|
|
||||||
except DucoError as err:
|
|
||||||
_LOGGER.debug("Could not fetch Duco LAN info", exc_info=err)
|
|
||||||
else:
|
|
||||||
rssi_wifi = lan_info.rssi_wifi
|
|
||||||
|
|
||||||
return DucoData(
|
return DucoData(
|
||||||
nodes={node.node_id: node for node in nodes},
|
nodes={node.node_id: node for node in nodes},
|
||||||
rssi_wifi=rssi_wifi,
|
rssi_wifi=lan_info.rssi_wifi,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -64,23 +64,23 @@
|
|||||||
"ventilation_state": {
|
"ventilation_state": {
|
||||||
"name": "Ventilation state",
|
"name": "Ventilation state",
|
||||||
"state": {
|
"state": {
|
||||||
"aut1": "AUT1",
|
"aut1": "Automatic boost (15 min)",
|
||||||
"aut2": "AUT2",
|
"aut2": "Automatic boost (30 min)",
|
||||||
"aut3": "AUT3",
|
"aut3": "Automatic boost (45 min)",
|
||||||
"auto": "AUTO",
|
"auto": "Automatic",
|
||||||
"cnt1": "CNT1",
|
"cnt1": "Continuous low speed",
|
||||||
"cnt2": "CNT2",
|
"cnt2": "Continuous medium speed",
|
||||||
"cnt3": "CNT3",
|
"cnt3": "Continuous high speed",
|
||||||
"empt": "EMPT",
|
"empt": "Empty house",
|
||||||
"man1": "MAN1",
|
"man1": "Manual low speed (15 min)",
|
||||||
"man1x2": "MAN1x2",
|
"man1x2": "Manual low speed (30 min)",
|
||||||
"man1x3": "MAN1x3",
|
"man1x3": "Manual low speed (45 min)",
|
||||||
"man2": "MAN2",
|
"man2": "Manual medium speed (15 min)",
|
||||||
"man2x2": "MAN2x2",
|
"man2x2": "Manual medium speed (30 min)",
|
||||||
"man2x3": "MAN2x3",
|
"man2x3": "Manual medium speed (45 min)",
|
||||||
"man3": "MAN3",
|
"man3": "Manual high speed (15 min)",
|
||||||
"man3x2": "MAN3x2",
|
"man3x2": "Manual high speed (30 min)",
|
||||||
"man3x3": "MAN3x3"
|
"man3x3": "Manual high speed (45 min)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from env_canada import ECAirQuality, ECMap, ECWeather
|
from env_canada import ECAirQuality, ECRadar, ECWeather
|
||||||
|
|
||||||
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) ->
|
|||||||
errors = errors + 1
|
errors = errors + 1
|
||||||
_LOGGER.warning("Unable to retrieve Environment Canada weather")
|
_LOGGER.warning("Unable to retrieve Environment Canada weather")
|
||||||
|
|
||||||
radar_data = ECMap(coordinates=(lat, lon), layer="precip_type", legend=False)
|
radar_data = ECRadar(coordinates=(lat, lon))
|
||||||
radar_coordinator = ECDataUpdateCoordinator(
|
radar_coordinator = ECDataUpdateCoordinator(
|
||||||
hass, config_entry, radar_data, "radar", DEFAULT_RADAR_UPDATE_INTERVAL
|
hass, config_entry, radar_data, "radar", DEFAULT_RADAR_UPDATE_INTERVAL
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Support for the Environment Canada radar imagery."""
|
"""Support for the Environment Canada radar imagery."""
|
||||||
|
|
||||||
from env_canada import ECMap
|
from env_canada import ECRadar
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera
|
||||||
@@ -11,20 +11,13 @@ from homeassistant.helpers.entity_platform import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import VolDictType
|
from homeassistant.helpers.typing import VolDictType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .const import ATTR_OBSERVATION_TIME
|
from .const import ATTR_OBSERVATION_TIME
|
||||||
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator
|
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator
|
||||||
|
|
||||||
SERVICE_SET_RADAR_TYPE = "set_radar_type"
|
SERVICE_SET_RADAR_TYPE = "set_radar_type"
|
||||||
SET_RADAR_TYPE_SCHEMA: VolDictType = {
|
SET_RADAR_TYPE_SCHEMA: VolDictType = {
|
||||||
vol.Required("radar_type"): vol.In(["Auto", "Rain", "Snow", "Precipitation type"]),
|
vol.Required("radar_type"): vol.In(["Auto", "Rain", "Snow"]),
|
||||||
}
|
|
||||||
|
|
||||||
_RADAR_TYPE_TO_LAYER: dict[str, str] = {
|
|
||||||
"Rain": "rain",
|
|
||||||
"Snow": "snow",
|
|
||||||
"Precipitation type": "precip_type",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -45,13 +38,13 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ECCameraEntity(CoordinatorEntity[ECDataUpdateCoordinator[ECMap]], Camera):
|
class ECCameraEntity(CoordinatorEntity[ECDataUpdateCoordinator[ECRadar]], Camera):
|
||||||
"""Implementation of an Environment Canada radar camera."""
|
"""Implementation of an Environment Canada radar camera."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_translation_key = "radar"
|
_attr_translation_key = "radar"
|
||||||
|
|
||||||
def __init__(self, coordinator: ECDataUpdateCoordinator[ECMap]) -> None:
|
def __init__(self, coordinator: ECDataUpdateCoordinator[ECRadar]) -> None:
|
||||||
"""Initialize the camera."""
|
"""Initialize the camera."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
Camera.__init__(self)
|
Camera.__init__(self)
|
||||||
@@ -83,13 +76,6 @@ class ECCameraEntity(CoordinatorEntity[ECDataUpdateCoordinator[ECMap]], Camera):
|
|||||||
|
|
||||||
async def async_set_radar_type(self, radar_type: str) -> None:
|
async def async_set_radar_type(self, radar_type: str) -> None:
|
||||||
"""Set the type of radar to retrieve."""
|
"""Set the type of radar to retrieve."""
|
||||||
if radar_type == "Auto":
|
|
||||||
# Choose rain for months April through October, snow otherwise
|
|
||||||
layer = "rain" if dt_util.now().month in range(4, 11) else "snow"
|
|
||||||
else:
|
|
||||||
layer = _RADAR_TYPE_TO_LAYER[radar_type]
|
|
||||||
|
|
||||||
# Apply new layer and clear cache to force refresh
|
|
||||||
self.radar_object.layer = layer
|
|
||||||
self.radar_object.clear_cache()
|
self.radar_object.clear_cache()
|
||||||
await self.coordinator.async_request_refresh()
|
self.radar_object.precip_type = radar_type.lower()
|
||||||
|
await self.radar_object.update()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from env_canada import ECAirQuality, ECMap, ECWeather, ECWeatherUpdateFailed, ec_exc
|
from env_canada import ECAirQuality, ECRadar, ECWeather, ECWeatherUpdateFailed, ec_exc
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -17,7 +17,7 @@ from .const import DOMAIN
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
type ECConfigEntry = ConfigEntry[ECRuntimeData]
|
type ECConfigEntry = ConfigEntry[ECRuntimeData]
|
||||||
type ECDataType = ECAirQuality | ECMap | ECWeather
|
type ECDataType = ECAirQuality | ECRadar | ECWeather
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -25,7 +25,7 @@ class ECRuntimeData:
|
|||||||
"""Class to hold EC runtime data."""
|
"""Class to hold EC runtime data."""
|
||||||
|
|
||||||
aqhi_coordinator: ECDataUpdateCoordinator[ECAirQuality]
|
aqhi_coordinator: ECDataUpdateCoordinator[ECAirQuality]
|
||||||
radar_coordinator: ECDataUpdateCoordinator[ECMap]
|
radar_coordinator: ECDataUpdateCoordinator[ECRadar]
|
||||||
weather_coordinator: ECDataUpdateCoordinator[ECWeather]
|
weather_coordinator: ECDataUpdateCoordinator[ECWeather]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ set_radar_type:
|
|||||||
fields:
|
fields:
|
||||||
radar_type:
|
radar_type:
|
||||||
required: true
|
required: true
|
||||||
example: Rain
|
example: Snow
|
||||||
selector:
|
selector:
|
||||||
select:
|
select:
|
||||||
options:
|
options:
|
||||||
- "Auto"
|
- "Auto"
|
||||||
- "Rain"
|
- "Rain"
|
||||||
- "Snow"
|
- "Snow"
|
||||||
- "Precipitation type"
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -21,5 +21,5 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"preview_features": { "winter_mode": {} },
|
"preview_features": { "winter_mode": {} },
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20260527.1"]
|
"requirements": ["home-assistant-frontend==20260429.4"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields: &trigger_common_fields
|
.trigger_common_fields: &trigger_common_fields
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields: &trigger_common_fields
|
.trigger_common_fields: &trigger_common_fields
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -199,7 +199,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
|||||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.RECEIVER): TYPE_RECEIVER,
|
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.RECEIVER): TYPE_RECEIVER,
|
||||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.SPEAKER): TYPE_SPEAKER,
|
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.SPEAKER): TYPE_SPEAKER,
|
||||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.TV): TYPE_TV,
|
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.TV): TYPE_TV,
|
||||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.PROJECTOR): TYPE_TV,
|
|
||||||
(sensor.DOMAIN, sensor.SensorDeviceClass.AQI): TYPE_SENSOR,
|
(sensor.DOMAIN, sensor.SensorDeviceClass.AQI): TYPE_SENSOR,
|
||||||
(sensor.DOMAIN, sensor.SensorDeviceClass.HUMIDITY): TYPE_SENSOR,
|
(sensor.DOMAIN, sensor.SensorDeviceClass.HUMIDITY): TYPE_SENSOR,
|
||||||
(sensor.DOMAIN, sensor.SensorDeviceClass.TEMPERATURE): TYPE_SENSOR,
|
(sensor.DOMAIN, sensor.SensorDeviceClass.TEMPERATURE): TYPE_SENSOR,
|
||||||
|
|||||||
@@ -2728,11 +2728,7 @@ class ChannelTrait(_Trait):
|
|||||||
if (
|
if (
|
||||||
domain == media_player.DOMAIN
|
domain == media_player.DOMAIN
|
||||||
and (features & MediaPlayerEntityFeature.PLAY_MEDIA)
|
and (features & MediaPlayerEntityFeature.PLAY_MEDIA)
|
||||||
and device_class
|
and device_class == media_player.MediaPlayerDeviceClass.TV
|
||||||
in (
|
|
||||||
media_player.MediaPlayerDeviceClass.TV,
|
|
||||||
media_player.MediaPlayerDeviceClass.PROJECTOR,
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ DEFAULT_STT_PROMPT = "Transcribe the attached audio"
|
|||||||
|
|
||||||
CONF_RECOMMENDED = "recommended"
|
CONF_RECOMMENDED = "recommended"
|
||||||
CONF_CHAT_MODEL = "chat_model"
|
CONF_CHAT_MODEL = "chat_model"
|
||||||
RECOMMENDED_CHAT_MODEL = "models/gemini-3.1-flash-lite"
|
RECOMMENDED_CHAT_MODEL = "models/gemini-2.5-flash"
|
||||||
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
|
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
|
||||||
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
|
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
|
||||||
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
|
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"name": "Home Assistant Hardware",
|
"name": "Home Assistant Hardware",
|
||||||
"after_dependencies": ["hassio"],
|
"after_dependencies": ["hassio"],
|
||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"dependencies": ["repairs", "usb"],
|
"dependencies": ["usb"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
"""Repairs for the Home Assistant Hardware integration."""
|
|
||||||
|
|
||||||
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers import issue_registry as ir
|
|
||||||
|
|
||||||
ISSUE_MULTI_PAN_MIGRATION = "multi_pan_migration"
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _multi_pan_issue_id(config_entry: ConfigEntry) -> str:
|
|
||||||
"""Return the issue id for the multi-PAN migration issue of an entry."""
|
|
||||||
return f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_create_multi_pan_migration_issue(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
domain: str,
|
|
||||||
config_entry: ConfigEntry,
|
|
||||||
) -> None:
|
|
||||||
"""Create a repair issue to guide migration away from Multi-PAN."""
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
domain=domain,
|
|
||||||
issue_id=_multi_pan_issue_id(config_entry),
|
|
||||||
is_fixable=True,
|
|
||||||
is_persistent=False,
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
|
||||||
translation_placeholders={"hardware_name": config_entry.title},
|
|
||||||
data={"entry_id": config_entry.entry_id},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_delete_multi_pan_migration_issue(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
domain: str,
|
|
||||||
config_entry: ConfigEntry,
|
|
||||||
) -> None:
|
|
||||||
"""Delete the multi-PAN migration repair issue for this entry."""
|
|
||||||
ir.async_delete_issue(hass, domain, _multi_pan_issue_id(config_entry))
|
|
||||||
|
|
||||||
|
|
||||||
class MultiPanMigrationRepairFlow(RepairsFlow):
|
|
||||||
"""Reuse the multi-PAN options flow uninstall steps as a repair flow.
|
|
||||||
|
|
||||||
Subclass this together with the hardware-specific
|
|
||||||
``MultiPanOptionsFlowHandler`` in each hardware integration's repairs
|
|
||||||
module.
|
|
||||||
|
|
||||||
The repair flow runs in the repairs flow manager where ``self.handler``
|
|
||||||
is the integration domain rather than the hardware config entry id, so
|
|
||||||
the ``config_entry`` accessor of ``OptionsFlow`` must be overridden.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_repair_config_entry: ConfigEntry
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config_entry(self) -> ConfigEntry:
|
|
||||||
"""Return the hardware config entry to migrate."""
|
|
||||||
return self._repair_config_entry
|
|
||||||
|
|
||||||
async def _async_step_start_migration(self) -> RepairsFlowResult:
|
|
||||||
"""Jump straight into the uninstall step of the migration flow.
|
|
||||||
|
|
||||||
The repair flow's init data is the issue context, not user form input,
|
|
||||||
so pass None to render the uninstall confirmation form.
|
|
||||||
"""
|
|
||||||
return await self.async_step_uninstall_addon() # type: ignore[attr-defined, no-any-return]
|
|
||||||
@@ -6,8 +6,6 @@ import dataclasses
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Protocol
|
from typing import Any, Protocol
|
||||||
|
|
||||||
from aiohttp import ClientError
|
|
||||||
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
|
||||||
@@ -27,7 +25,6 @@ from homeassistant.config_entries import (
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.data_entry_flow import AbortFlow
|
from homeassistant.data_entry_flow import AbortFlow
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.integration_platform import (
|
from homeassistant.helpers.integration_platform import (
|
||||||
async_process_integration_platforms,
|
async_process_integration_platforms,
|
||||||
@@ -40,18 +37,15 @@ from homeassistant.helpers.selector import (
|
|||||||
from homeassistant.helpers.singleton import singleton
|
from homeassistant.helpers.singleton import singleton
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
from .const import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||||
from .util import (
|
|
||||||
ApplicationType,
|
|
||||||
WaitingAddonManager,
|
|
||||||
async_firmware_flashing_context,
|
|
||||||
async_flash_silabs_firmware,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_MULTIPROTOCOL_ADDON_MANAGER = "silabs_multiprotocol_addon_manager"
|
DATA_MULTIPROTOCOL_ADDON_MANAGER = "silabs_multiprotocol_addon_manager"
|
||||||
|
DATA_FLASHER_ADDON_MANAGER = "silabs_flasher"
|
||||||
|
|
||||||
|
ADDON_STATE_POLL_INTERVAL = 3
|
||||||
|
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
||||||
|
|
||||||
CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware"
|
CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware"
|
||||||
CONF_ADDON_DEVICE = "device"
|
CONF_ADDON_DEVICE = "device"
|
||||||
@@ -77,6 +71,53 @@ async def get_multiprotocol_addon_manager(
|
|||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
class WaitingAddonManager(AddonManager):
|
||||||
|
"""Addon manager which supports waiting operations for managing an addon."""
|
||||||
|
|
||||||
|
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
||||||
|
"""Poll an addon's info until it is in a specific state."""
|
||||||
|
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
info = await self.async_get_addon_info()
|
||||||
|
except AddonError:
|
||||||
|
info = None
|
||||||
|
|
||||||
|
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
||||||
|
|
||||||
|
if info is not None and info.state in states:
|
||||||
|
break
|
||||||
|
|
||||||
|
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
||||||
|
|
||||||
|
async def async_start_addon_waiting(self) -> None:
|
||||||
|
"""Start an add-on."""
|
||||||
|
await self.async_schedule_start_addon()
|
||||||
|
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
||||||
|
|
||||||
|
async def async_install_addon_waiting(self) -> None:
|
||||||
|
"""Install an add-on."""
|
||||||
|
await self.async_schedule_install_addon()
|
||||||
|
await self.async_wait_until_addon_state(
|
||||||
|
AddonState.RUNNING,
|
||||||
|
AddonState.NOT_RUNNING,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_uninstall_addon_waiting(self) -> None:
|
||||||
|
"""Uninstall an add-on."""
|
||||||
|
try:
|
||||||
|
info = await self.async_get_addon_info()
|
||||||
|
except AddonError:
|
||||||
|
info = None
|
||||||
|
|
||||||
|
# Do not try to uninstall an addon if it is already uninstalled
|
||||||
|
if info is not None and info.state is AddonState.NOT_INSTALLED:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.async_uninstall_addon()
|
||||||
|
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
||||||
|
|
||||||
|
|
||||||
class MultiprotocolAddonManager(WaitingAddonManager):
|
class MultiprotocolAddonManager(WaitingAddonManager):
|
||||||
"""Silicon Labs Multiprotocol add-on manager."""
|
"""Silicon Labs Multiprotocol add-on manager."""
|
||||||
|
|
||||||
@@ -224,6 +265,18 @@ class MultipanProtocol(Protocol):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@singleton(DATA_FLASHER_ADDON_MANAGER)
|
||||||
|
@callback
|
||||||
|
def get_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
||||||
|
"""Get the flasher add-on manager."""
|
||||||
|
return WaitingAddonManager(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
"Silicon Labs Flasher",
|
||||||
|
SILABS_FLASHER_ADDON_SLUG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class SerialPortSettings:
|
class SerialPortSettings:
|
||||||
"""Serial port settings."""
|
"""Serial port settings."""
|
||||||
@@ -286,19 +339,6 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
|||||||
def _zha_name(self) -> str:
|
def _zha_name(self) -> str:
|
||||||
"""Return the ZHA name."""
|
"""Return the ZHA name."""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _firmware_update_url(self) -> str:
|
|
||||||
"""Return the firmware update manifest URL."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _zigbee_firmware_type(self) -> str:
|
|
||||||
"""Return the zigbee firmware type identifier (e.g. 'yellow_zigbee_ncp')."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def _flasher_cls(self) -> type:
|
|
||||||
"""Return the hardware-specific flasher class."""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def flow_manager(self) -> OptionsFlowManager:
|
def flow_manager(self) -> OptionsFlowManager:
|
||||||
"""Return the correct flow manager."""
|
"""Return the correct flow manager."""
|
||||||
@@ -646,7 +686,61 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
|||||||
async def async_step_firmware_revert(
|
async def async_step_firmware_revert(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Initiate ZHA backup and start multiprotocol addon uninstall."""
|
"""Install the flasher addon, if necessary."""
|
||||||
|
|
||||||
|
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||||
|
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||||
|
|
||||||
|
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||||
|
return await self.async_step_install_flasher_addon()
|
||||||
|
|
||||||
|
if addon_info.state is AddonState.NOT_RUNNING:
|
||||||
|
return await self.async_step_configure_flasher_addon()
|
||||||
|
|
||||||
|
# If the addon is already installed and running, fail
|
||||||
|
return self.async_abort(
|
||||||
|
reason="addon_already_running",
|
||||||
|
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_install_flasher_addon(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Show progress dialog for installing flasher addon."""
|
||||||
|
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||||
|
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||||
|
|
||||||
|
_LOGGER.debug("Flasher addon state: %s", addon_info)
|
||||||
|
|
||||||
|
if not self.install_task:
|
||||||
|
self.install_task = self.hass.async_create_task(
|
||||||
|
flasher_manager.async_install_addon_waiting(),
|
||||||
|
"SiLabs Flasher addon install",
|
||||||
|
eager_start=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.install_task.done():
|
||||||
|
return self.async_show_progress(
|
||||||
|
step_id="install_flasher_addon",
|
||||||
|
progress_action="install_addon",
|
||||||
|
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||||
|
progress_task=self.install_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.install_task
|
||||||
|
except AddonError as err:
|
||||||
|
_LOGGER.error(err)
|
||||||
|
return self.async_show_progress_done(next_step_id="install_failed")
|
||||||
|
finally:
|
||||||
|
self.install_task = None
|
||||||
|
|
||||||
|
return self.async_show_progress_done(next_step_id="configure_flasher_addon")
|
||||||
|
|
||||||
|
async def async_step_configure_flasher_addon(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Perform initial backup and reconfigure ZHA."""
|
||||||
# pylint: disable=home-assistant-component-root-import
|
# pylint: disable=home-assistant-component-root-import
|
||||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||||
@@ -688,6 +782,17 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
|||||||
_LOGGER.exception("Unexpected exception during ZHA migration")
|
_LOGGER.exception("Unexpected exception during ZHA migration")
|
||||||
raise AbortFlow("zha_migration_failed") from err
|
raise AbortFlow("zha_migration_failed") from err
|
||||||
|
|
||||||
|
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||||
|
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||||
|
new_addon_config = {
|
||||||
|
**addon_info.options,
|
||||||
|
"device": new_settings.device,
|
||||||
|
"flow_control": new_settings.flow_control,
|
||||||
|
}
|
||||||
|
|
||||||
|
_LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config)
|
||||||
|
await self._async_set_addon_config(new_addon_config, flasher_manager)
|
||||||
|
|
||||||
return await self.async_step_uninstall_multiprotocol_addon()
|
return await self.async_step_uninstall_multiprotocol_addon()
|
||||||
|
|
||||||
async def async_step_uninstall_multiprotocol_addon(
|
async def async_step_uninstall_multiprotocol_addon(
|
||||||
@@ -716,93 +821,62 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
|||||||
finally:
|
finally:
|
||||||
self.stop_task = None
|
self.stop_task = None
|
||||||
|
|
||||||
return self.async_show_progress_done(next_step_id="install_zigbee_firmware")
|
return self.async_show_progress_done(next_step_id="start_flasher_addon")
|
||||||
|
|
||||||
async def async_step_install_zigbee_firmware(
|
async def async_step_start_flasher_addon(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Flash Zigbee firmware directly onto the radio."""
|
"""Start Silicon Labs Flasher add-on."""
|
||||||
if not self.install_task:
|
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||||
|
|
||||||
async def _flash_firmware() -> None:
|
if not self.start_task:
|
||||||
serial_port_settings = await self._async_serial_port_settings()
|
|
||||||
device = serial_port_settings.device
|
|
||||||
|
|
||||||
# For the duration of firmware flashing, hint to other integrations
|
async def start_and_wait_until_done() -> None:
|
||||||
# (i.e. ZHA) that the hardware is in use and should not be accessed.
|
await flasher_manager.async_start_addon_waiting()
|
||||||
async with async_firmware_flashing_context(self.hass, device, DOMAIN):
|
# Now that the addon is running, wait for it to finish
|
||||||
session = async_get_clientsession(self.hass)
|
await flasher_manager.async_wait_until_addon_state(
|
||||||
client = FirmwareUpdateClient(self._firmware_update_url(), session)
|
AddonState.NOT_RUNNING
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
self.start_task = self.hass.async_create_task(
|
||||||
manifest = await client.async_update_data()
|
start_and_wait_until_done(), eager_start=False
|
||||||
fw_manifest = next(
|
|
||||||
fw
|
|
||||||
for fw in manifest.firmwares
|
|
||||||
if fw.filename.startswith(self._zigbee_firmware_type())
|
|
||||||
)
|
|
||||||
fw_data = await client.async_fetch_firmware(fw_manifest)
|
|
||||||
except (
|
|
||||||
StopIteration,
|
|
||||||
TimeoutError,
|
|
||||||
ClientError,
|
|
||||||
ManifestMissing,
|
|
||||||
ValueError,
|
|
||||||
) as err:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
"Failed to fetch Zigbee firmware"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
await async_flash_silabs_firmware(
|
|
||||||
hass=self.hass,
|
|
||||||
device=device,
|
|
||||||
fw_data=fw_data,
|
|
||||||
flasher_cls=self._flasher_cls,
|
|
||||||
expected_installed_firmware_type=ApplicationType.EZSP,
|
|
||||||
progress_callback=lambda offset, total: (
|
|
||||||
self.async_update_progress(offset / total)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.install_task = self.hass.async_create_task(
|
|
||||||
_flash_firmware(),
|
|
||||||
"Flash Zigbee firmware",
|
|
||||||
eager_start=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.install_task.done():
|
if not self.start_task.done():
|
||||||
return self.async_show_progress(
|
return self.async_show_progress(
|
||||||
step_id="install_zigbee_firmware",
|
step_id="start_flasher_addon",
|
||||||
progress_action="install_zigbee_firmware",
|
progress_action="start_flasher_addon",
|
||||||
description_placeholders={
|
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||||
"hardware_name": self._hardware_name(),
|
progress_task=self.start_task,
|
||||||
},
|
|
||||||
progress_task=self.install_task,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.install_task
|
await self.start_task
|
||||||
except HomeAssistantError as err:
|
except (AddonError, AbortFlow) as err:
|
||||||
_LOGGER.error("Failed to flash Zigbee firmware: %s", err)
|
_LOGGER.error(err)
|
||||||
return self.async_show_progress_done(next_step_id="firmware_flash_failed")
|
return self.async_show_progress_done(next_step_id="flasher_failed")
|
||||||
finally:
|
finally:
|
||||||
self.install_task = None
|
self.start_task = None
|
||||||
|
|
||||||
return self.async_show_progress_done(next_step_id="flashing_complete")
|
return self.async_show_progress_done(next_step_id="flashing_complete")
|
||||||
|
|
||||||
async def async_step_firmware_flash_failed(
|
async def async_step_flasher_failed(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Firmware flashing failed."""
|
"""Flasher add-on start failed."""
|
||||||
|
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||||
return self.async_abort(
|
return self.async_abort(
|
||||||
reason="fw_install_failed",
|
reason="addon_start_failed",
|
||||||
description_placeholders={"firmware_name": "Zigbee"},
|
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_flashing_complete(
|
async def async_step_flashing_complete(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Finish flashing and update the config entry."""
|
"""Finish flashing and update the config entry."""
|
||||||
|
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||||
|
await flasher_manager.async_uninstall_addon_waiting()
|
||||||
|
|
||||||
# Finish ZHA migration if needed
|
# Finish ZHA migration if needed
|
||||||
if self._zha_migration_mgr:
|
if self._zha_migration_mgr:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -102,9 +102,7 @@
|
|||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"install_addon": "Please wait while the {addon_name} app installation finishes. This can take several minutes.",
|
"install_addon": "Please wait while the {addon_name} app installation finishes. This can take several minutes.",
|
||||||
"install_zigbee_firmware": "Please wait while Zigbee-only firmware is installed on your {hardware_name}. This can take several minutes.",
|
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds."
|
||||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds.",
|
|
||||||
"uninstall_multiprotocol_addon": "Please wait while the {addon_name} app is uninstalled."
|
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"addon_installed_other_device": {
|
"addon_installed_other_device": {
|
||||||
|
|||||||
@@ -37,59 +37,13 @@ from .const import (
|
|||||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||||
)
|
)
|
||||||
from .helpers import async_firmware_update_context
|
from .helpers import async_firmware_update_context
|
||||||
|
from .silabs_multiprotocol_addon import (
|
||||||
|
WaitingAddonManager,
|
||||||
|
get_multiprotocol_addon_manager,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ADDON_STATE_POLL_INTERVAL = 3
|
|
||||||
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
|
||||||
|
|
||||||
|
|
||||||
class WaitingAddonManager(AddonManager):
|
|
||||||
"""Addon manager which supports waiting operations for managing an addon."""
|
|
||||||
|
|
||||||
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
|
||||||
"""Poll an addon's info until it is in a specific state."""
|
|
||||||
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
info = await self.async_get_addon_info()
|
|
||||||
except AddonError:
|
|
||||||
info = None
|
|
||||||
|
|
||||||
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
|
||||||
|
|
||||||
if info is not None and info.state in states:
|
|
||||||
break
|
|
||||||
|
|
||||||
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
|
||||||
|
|
||||||
async def async_start_addon_waiting(self) -> None:
|
|
||||||
"""Start an add-on."""
|
|
||||||
await self.async_schedule_start_addon()
|
|
||||||
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
|
||||||
|
|
||||||
async def async_install_addon_waiting(self) -> None:
|
|
||||||
"""Install an add-on."""
|
|
||||||
await self.async_schedule_install_addon()
|
|
||||||
await self.async_wait_until_addon_state(
|
|
||||||
AddonState.RUNNING,
|
|
||||||
AddonState.NOT_RUNNING,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_uninstall_addon_waiting(self) -> None:
|
|
||||||
"""Uninstall an add-on."""
|
|
||||||
try:
|
|
||||||
info = await self.async_get_addon_info()
|
|
||||||
except AddonError:
|
|
||||||
info = None
|
|
||||||
|
|
||||||
# Do not try to uninstall an addon if it is already uninstalled
|
|
||||||
if info is not None and info.state == AddonState.NOT_INSTALLED:
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.async_uninstall_addon()
|
|
||||||
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationType(StrEnum):
|
class ApplicationType(StrEnum):
|
||||||
"""Application type running on a device."""
|
"""Application type running on a device."""
|
||||||
@@ -325,11 +279,6 @@ async def guess_hardware_owners(
|
|||||||
assert otbr_addon_fw_info is not None
|
assert otbr_addon_fw_info is not None
|
||||||
device_guesses[otbr_path].append(otbr_addon_fw_info)
|
device_guesses[otbr_path].append(otbr_addon_fw_info)
|
||||||
|
|
||||||
# Lazy import to avoid circular dependency
|
|
||||||
from .silabs_multiprotocol_addon import ( # noqa: PLC0415
|
|
||||||
get_multiprotocol_addon_manager,
|
|
||||||
)
|
|
||||||
|
|
||||||
multipan_addon_manager = await get_multiprotocol_addon_manager(hass)
|
multipan_addon_manager = await get_multiprotocol_addon_manager(hass)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ import os.path
|
|||||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||||
FirmwareUpdateCoordinator,
|
FirmwareUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
|
||||||
async_create_multi_pan_migration_issue,
|
|
||||||
async_delete_multi_pan_migration_issue,
|
|
||||||
)
|
|
||||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
|
||||||
multi_pan_addon_using_device,
|
|
||||||
)
|
|
||||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||||
from homeassistant.components.usb import (
|
from homeassistant.components.usb import (
|
||||||
USBDevice,
|
USBDevice,
|
||||||
@@ -99,16 +92,6 @@ async def async_setup_entry(
|
|||||||
translation_key="device_disconnected",
|
translation_key="device_disconnected",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
uses_multi_pan = await multi_pan_addon_using_device(hass, device_path)
|
|
||||||
except HomeAssistantError as err:
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
|
|
||||||
if uses_multi_pan:
|
|
||||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
|
||||||
else:
|
|
||||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
|
||||||
|
|
||||||
# Create and store the firmware update coordinator in runtime_data
|
# Create and store the firmware update coordinator in runtime_data
|
||||||
session = async_get_clientsession(hass)
|
session = async_get_clientsession(hass)
|
||||||
coordinator = FirmwareUpdateCoordinator(
|
coordinator = FirmwareUpdateCoordinator(
|
||||||
|
|||||||
@@ -248,19 +248,6 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler(
|
|||||||
"""Return the name of the hardware."""
|
"""Return the name of the hardware."""
|
||||||
return self._hw_variant.full_name
|
return self._hw_variant.full_name
|
||||||
|
|
||||||
def _firmware_update_url(self) -> str:
|
|
||||||
"""Return the firmware update manifest URL."""
|
|
||||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
|
||||||
|
|
||||||
def _zigbee_firmware_type(self) -> str:
|
|
||||||
"""Return the zigbee firmware type identifier."""
|
|
||||||
return "skyconnect_zigbee_ncp"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _flasher_cls(self) -> type:
|
|
||||||
"""Return the hardware-specific flasher class."""
|
|
||||||
return Zbt1Flasher # type: ignore[no-any-return]
|
|
||||||
|
|
||||||
async def async_step_flashing_complete(
|
async def async_step_flashing_complete(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
"""Repairs for the Home Assistant SkyConnect integration."""
|
|
||||||
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
|
||||||
ISSUE_MULTI_PAN_MIGRATION,
|
|
||||||
MultiPanMigrationRepairFlow,
|
|
||||||
)
|
|
||||||
from homeassistant.components.repairs import (
|
|
||||||
ConfirmRepairFlow,
|
|
||||||
RepairsFlow,
|
|
||||||
RepairsFlowResult,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .config_flow import HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
|
||||||
|
|
||||||
|
|
||||||
class SkyConnectMultiPanMigrationRepairFlow(
|
|
||||||
MultiPanMigrationRepairFlow, HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
|
||||||
):
|
|
||||||
"""Multi-PAN migration repair flow for Home Assistant SkyConnect."""
|
|
||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
|
||||||
"""Initialize the repair flow."""
|
|
||||||
HomeAssistantSkyConnectMultiPanOptionsFlowHandler.__init__(self, config_entry)
|
|
||||||
self._repair_config_entry = config_entry
|
|
||||||
|
|
||||||
async def async_step_init( # type: ignore[override]
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> RepairsFlowResult:
|
|
||||||
"""Jump straight into the uninstall step."""
|
|
||||||
return await self._async_step_start_migration()
|
|
||||||
|
|
||||||
|
|
||||||
async def async_create_fix_flow(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
issue_id: str,
|
|
||||||
data: dict[str, str | int | float | None] | None,
|
|
||||||
) -> RepairsFlow:
|
|
||||||
"""Create a fix flow for a SkyConnect repair issue."""
|
|
||||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
|
||||||
entry_id = cast(str, data["entry_id"])
|
|
||||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
|
||||||
return SkyConnectMultiPanMigrationRepairFlow(entry)
|
|
||||||
|
|
||||||
return ConfirmRepairFlow()
|
|
||||||
@@ -106,37 +106,6 @@
|
|||||||
"message": "The device is not plugged in"
|
"message": "The device is not plugged in"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
|
||||||
"multi_pan_migration": {
|
|
||||||
"fix_flow": {
|
|
||||||
"abort": {
|
|
||||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
|
||||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
|
||||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
|
||||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
|
||||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
|
||||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
|
||||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
|
||||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
|
||||||
},
|
|
||||||
"progress": {
|
|
||||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
|
||||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
|
||||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"uninstall_addon": {
|
|
||||||
"data": {
|
|
||||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
|
||||||
},
|
|
||||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
|
||||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Multiprotocol support is deprecated"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"options": {
|
"options": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||||
@@ -161,10 +130,8 @@
|
|||||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
|
||||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"addon_installed_other_device": {
|
"addon_installed_other_device": {
|
||||||
|
|||||||
@@ -7,13 +7,8 @@ from homeassistant.components.hassio import HassioNotReadyError, get_os_info
|
|||||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||||
FirmwareUpdateCoordinator,
|
FirmwareUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
|
||||||
async_create_multi_pan_migration_issue,
|
|
||||||
async_delete_multi_pan_migration_issue,
|
|
||||||
)
|
|
||||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||||
check_multi_pan_addon,
|
check_multi_pan_addon,
|
||||||
multi_pan_addon_using_device,
|
|
||||||
)
|
)
|
||||||
from homeassistant.components.homeassistant_hardware.util import (
|
from homeassistant.components.homeassistant_hardware.util import (
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
@@ -32,7 +27,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
|
||||||
FIRMWARE,
|
FIRMWARE,
|
||||||
FIRMWARE_VERSION,
|
FIRMWARE_VERSION,
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
@@ -83,16 +77,6 @@ async def async_setup_entry(
|
|||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
try:
|
|
||||||
multipan_using_device = await multi_pan_addon_using_device(hass, RADIO_DEVICE)
|
|
||||||
except HomeAssistantError as err:
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
|
|
||||||
if multipan_using_device:
|
|
||||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
|
||||||
else:
|
|
||||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
|
||||||
|
|
||||||
if firmware is ApplicationType.EZSP:
|
if firmware is ApplicationType.EZSP:
|
||||||
discovery_flow.async_create_flow(
|
discovery_flow.async_create_flow(
|
||||||
hass,
|
hass,
|
||||||
|
|||||||
@@ -319,19 +319,6 @@ class HomeAssistantYellowMultiPanOptionsFlowHandler(
|
|||||||
"""Return the name of the hardware."""
|
"""Return the name of the hardware."""
|
||||||
return BOARD_NAME
|
return BOARD_NAME
|
||||||
|
|
||||||
def _firmware_update_url(self) -> str:
|
|
||||||
"""Return the firmware update manifest URL."""
|
|
||||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
|
||||||
|
|
||||||
def _zigbee_firmware_type(self) -> str:
|
|
||||||
"""Return the zigbee firmware type identifier."""
|
|
||||||
return "yellow_zigbee_ncp"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _flasher_cls(self) -> type:
|
|
||||||
"""Return the hardware-specific flasher class."""
|
|
||||||
return YellowFlasher # type: ignore[no-any-return]
|
|
||||||
|
|
||||||
async def async_step_flashing_complete(
|
async def async_step_flashing_complete(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
"""Repairs for the Home Assistant Yellow integration."""
|
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
|
||||||
ISSUE_MULTI_PAN_MIGRATION,
|
|
||||||
MultiPanMigrationRepairFlow,
|
|
||||||
)
|
|
||||||
from homeassistant.components.repairs import (
|
|
||||||
ConfirmRepairFlow,
|
|
||||||
RepairsFlow,
|
|
||||||
RepairsFlowResult,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .config_flow import HomeAssistantYellowMultiPanOptionsFlowHandler
|
|
||||||
|
|
||||||
|
|
||||||
class YellowMultiPanMigrationRepairFlow(
|
|
||||||
MultiPanMigrationRepairFlow, HomeAssistantYellowMultiPanOptionsFlowHandler
|
|
||||||
):
|
|
||||||
"""Multi-PAN migration repair flow for Home Assistant Yellow."""
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
|
||||||
"""Initialize the repair flow."""
|
|
||||||
HomeAssistantYellowMultiPanOptionsFlowHandler.__init__(self, hass, config_entry)
|
|
||||||
self._repair_config_entry = config_entry
|
|
||||||
|
|
||||||
async def async_step_main_menu( # type: ignore[override]
|
|
||||||
self, _: None = None
|
|
||||||
) -> RepairsFlowResult:
|
|
||||||
"""Jump straight into the uninstall step."""
|
|
||||||
return await self._async_step_start_migration()
|
|
||||||
|
|
||||||
|
|
||||||
async def async_create_fix_flow(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
issue_id: str,
|
|
||||||
data: dict[str, str | int | float | None] | None,
|
|
||||||
) -> RepairsFlow:
|
|
||||||
"""Create a fix flow for a Yellow repair issue."""
|
|
||||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
|
||||||
entry_id = cast(str, data["entry_id"])
|
|
||||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
|
||||||
return YellowMultiPanMigrationRepairFlow(hass, entry)
|
|
||||||
|
|
||||||
return ConfirmRepairFlow()
|
|
||||||
@@ -11,37 +11,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
|
||||||
"multi_pan_migration": {
|
|
||||||
"fix_flow": {
|
|
||||||
"abort": {
|
|
||||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
|
||||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
|
||||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
|
||||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
|
||||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
|
||||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
|
||||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
|
||||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
|
||||||
},
|
|
||||||
"progress": {
|
|
||||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
|
||||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
|
||||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"uninstall_addon": {
|
|
||||||
"data": {
|
|
||||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
|
||||||
},
|
|
||||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
|
||||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Multiprotocol support is deprecated"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"options": {
|
"options": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||||
@@ -68,10 +37,8 @@
|
|||||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
|
||||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"addon_installed_other_device": {
|
"addon_installed_other_device": {
|
||||||
|
|||||||
@@ -202,10 +202,7 @@ def get_accessory( # noqa: C901
|
|||||||
|
|
||||||
if device_class == MediaPlayerDeviceClass.RECEIVER:
|
if device_class == MediaPlayerDeviceClass.RECEIVER:
|
||||||
a_type = "ReceiverMediaPlayer"
|
a_type = "ReceiverMediaPlayer"
|
||||||
elif device_class in (
|
elif device_class == MediaPlayerDeviceClass.TV:
|
||||||
MediaPlayerDeviceClass.TV,
|
|
||||||
MediaPlayerDeviceClass.PROJECTOR,
|
|
||||||
):
|
|
||||||
a_type = "TelevisionMediaPlayer"
|
a_type = "TelevisionMediaPlayer"
|
||||||
elif validate_media_player_features(state, feature_list):
|
elif validate_media_player_features(state, feature_list):
|
||||||
a_type = "MediaPlayer"
|
a_type = "MediaPlayer"
|
||||||
|
|||||||
@@ -695,11 +695,7 @@ def state_needs_accessory_mode(state: State) -> bool:
|
|||||||
return (
|
return (
|
||||||
state.domain == MEDIA_PLAYER_DOMAIN
|
state.domain == MEDIA_PLAYER_DOMAIN
|
||||||
and state.attributes.get(ATTR_DEVICE_CLASS)
|
and state.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
in (
|
in (MediaPlayerDeviceClass.TV, MediaPlayerDeviceClass.RECEIVER)
|
||||||
MediaPlayerDeviceClass.TV,
|
|
||||||
MediaPlayerDeviceClass.RECEIVER,
|
|
||||||
MediaPlayerDeviceClass.PROJECTOR,
|
|
||||||
)
|
|
||||||
) or (
|
) or (
|
||||||
state.domain == REMOTE_DOMAIN
|
state.domain == REMOTE_DOMAIN
|
||||||
and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|||||||
@@ -178,21 +178,17 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
@property
|
@property
|
||||||
def max_color_temp_mireds(self) -> int:
|
def max_color_temp_mireds(self) -> int:
|
||||||
"""Return the warmest color_temp in mireds that this light supports."""
|
"""Return the warmest color_temp in mireds that this light supports."""
|
||||||
if (color_temp := self.resource.color_temperature) and (
|
if color_temp := self.resource.color_temperature:
|
||||||
mirek_max := color_temp.mirek_schema.mirek_maximum
|
return color_temp.mirek_schema.mirek_maximum
|
||||||
):
|
# return a fallback value if the light doesn't provide limits
|
||||||
return mirek_max
|
|
||||||
# return a fallback value if the light doesn't provide valid limits
|
|
||||||
return FALLBACK_MAX_MIREDS
|
return FALLBACK_MAX_MIREDS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_color_temp_mireds(self) -> int:
|
def min_color_temp_mireds(self) -> int:
|
||||||
"""Return the coldest color_temp in mireds that this light supports."""
|
"""Return the coldest color_temp in mireds that this light supports."""
|
||||||
if (color_temp := self.resource.color_temperature) and (
|
if color_temp := self.resource.color_temperature:
|
||||||
mirek_min := color_temp.mirek_schema.mirek_minimum
|
return color_temp.mirek_schema.mirek_minimum
|
||||||
):
|
# return a fallback value if the light doesn't provide limits
|
||||||
return mirek_min
|
|
||||||
# return a fallback value if the light doesn't provide valid limits
|
|
||||||
return FALLBACK_MIN_MIREDS
|
return FALLBACK_MIN_MIREDS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers.automation import DomainSpec
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
from homeassistant.helpers.entity import get_supported_features
|
from homeassistant.helpers.entity import get_supported_features
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import (
|
||||||
ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR,
|
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||||
EntityTargetStateTriggerBase,
|
EntityTargetStateTriggerBase,
|
||||||
Trigger,
|
Trigger,
|
||||||
TriggerConfig,
|
TriggerConfig,
|
||||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.trigger import (
|
|||||||
|
|
||||||
from .const import ATTR_ACTION, DOMAIN, HumidifierAction, HumidifierEntityFeature
|
from .const import ATTR_ACTION, DOMAIN, HumidifierAction, HumidifierEntityFeature
|
||||||
|
|
||||||
MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR.extend(
|
MODE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_OPTIONS): {
|
vol.Required(CONF_OPTIONS): {
|
||||||
vol.Required(CONF_MODE): vol.All(cv.ensure_list, vol.Length(min=1), [str]),
|
vol.Required(CONF_MODE): vol.All(cv.ensure_list, vol.Length(min=1), [str]),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
behavior: &trigger_behavior
|
behavior: &trigger_behavior
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields:
|
.trigger_common_fields:
|
||||||
behavior: &trigger_behavior
|
behavior: &trigger_behavior
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ CONF_INFO = "info"
|
|||||||
CONF_INVERTING = "inverting"
|
CONF_INVERTING = "inverting"
|
||||||
CONF_LIGHT = "light"
|
CONF_LIGHT = "light"
|
||||||
CONF_NODE = "node"
|
CONF_NODE = "node"
|
||||||
|
CONF_NOTE = "note"
|
||||||
CONF_OFF_ID = "off_id"
|
CONF_OFF_ID = "off_id"
|
||||||
CONF_ON_ID = "on_id"
|
CONF_ON_ID = "on_id"
|
||||||
CONF_POSITION = "position"
|
CONF_POSITION = "position"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_NOTE,
|
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
@@ -26,6 +25,7 @@ from .const import (
|
|||||||
CONF_INFO,
|
CONF_INFO,
|
||||||
CONF_INVERTING,
|
CONF_INVERTING,
|
||||||
CONF_LIGHT,
|
CONF_LIGHT,
|
||||||
|
CONF_NOTE,
|
||||||
CONF_OFF_ID,
|
CONF_OFF_ID,
|
||||||
CONF_ON_ID,
|
CONF_ON_ID,
|
||||||
CONF_POSITION,
|
CONF_POSITION,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.trigger_common_fields: &trigger_common_fields
|
.trigger_common_fields: &trigger_common_fields
|
||||||
behavior: &trigger_behavior
|
behavior: &trigger_behavior
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, override
|
from typing import Any
|
||||||
|
|
||||||
from incomfortclient import Heater as InComfortHeater
|
from incomfortclient import Heater as InComfortHeater
|
||||||
|
|
||||||
@@ -97,13 +97,11 @@ class IncomfortBinarySensor(IncomfortBoilerEntity, BinarySensorEntity):
|
|||||||
self._attr_unique_id = f"{heater.serial_no}_{description.key}"
|
self._attr_unique_id = f"{heater.serial_no}_{description.key}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return the status of the sensor."""
|
"""Return the status of the sensor."""
|
||||||
return bool(self._heater.status[self.entity_description.value_key])
|
return bool(self._heater.status[self.entity_description.value_key])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return the device state attributes."""
|
"""Return the device state attributes."""
|
||||||
if (attributes_fn := self.entity_description.extra_state_attributes_fn) is None:
|
if (attributes_fn := self.entity_description.extra_state_attributes_fn) is None:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway."""
|
"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway."""
|
||||||
|
|
||||||
from typing import Any, override
|
from typing import Any
|
||||||
|
|
||||||
from incomfortclient import Heater as InComfortHeater, Room as InComfortRoom
|
from incomfortclient import Heater as InComfortHeater, Room as InComfortRoom
|
||||||
|
|
||||||
@@ -76,19 +76,16 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
"""Return the device state attributes."""
|
"""Return the device state attributes."""
|
||||||
return {"status": self._room.status}
|
return {"status": self._room.status}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return self._room.room_temp
|
return self._room.room_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def hvac_action(self) -> HVACAction | None:
|
def hvac_action(self) -> HVACAction | None:
|
||||||
"""Return the actual current HVAC action."""
|
"""Return the actual current HVAC action."""
|
||||||
if self._heater.is_burning and self._heater.is_pumping:
|
if self._heater.is_burning and self._heater.is_pumping:
|
||||||
@@ -96,7 +93,6 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
|
|||||||
return HVACAction.IDLE
|
return HVACAction.IDLE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the (override)temperature we try to reach.
|
"""Return the (override)temperature we try to reach.
|
||||||
|
|
||||||
@@ -110,13 +106,11 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
|
|||||||
return self._room.setpoint
|
return self._room.setpoint
|
||||||
return self._room.override or self._room.setpoint
|
return self._room.override or self._room.setpoint
|
||||||
|
|
||||||
@override
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set a new target temperature for this zone."""
|
"""Set a new target temperature for this zone."""
|
||||||
temperature: float = kwargs[ATTR_TEMPERATURE]
|
temperature: float = kwargs[ATTR_TEMPERATURE]
|
||||||
await self._room.set_override(temperature)
|
await self._room.set_override(temperature)
|
||||||
await self.coordinator.async_refresh()
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
@override
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||||
"""Set new target hvac mode."""
|
"""Set new target hvac mode."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, override
|
from typing import Any
|
||||||
|
|
||||||
from incomfortclient import InvalidGateway, InvalidHeaterList
|
from incomfortclient import InvalidGateway, InvalidHeaterList
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -100,7 +100,6 @@ class InComfortConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
_discovered_host: str
|
_discovered_host: str
|
||||||
|
|
||||||
@override
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
@@ -109,7 +108,6 @@ class InComfortConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return InComfortOptionsFlowHandler()
|
return InComfortOptionsFlowHandler()
|
||||||
|
|
||||||
@override
|
|
||||||
async def async_step_dhcp(
|
async def async_step_dhcp(
|
||||||
self, discovery_info: DhcpServiceInfo
|
self, discovery_info: DhcpServiceInfo
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@@ -171,7 +169,6 @@ class InComfortConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
description_placeholders={CONF_HOST: self._discovered_host},
|
description_placeholders={CONF_HOST: self._discovered_host},
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, override
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
from incomfortclient import (
|
from incomfortclient import (
|
||||||
@@ -74,7 +74,6 @@ class InComfortDataCoordinator(DataUpdateCoordinator[InComfortData]):
|
|||||||
)
|
)
|
||||||
self.incomfort_data = incomfort_data
|
self.incomfort_data = incomfort_data
|
||||||
|
|
||||||
@override
|
|
||||||
async def _async_update_data(self) -> InComfortData:
|
async def _async_update_data(self) -> InComfortData:
|
||||||
"""Fetch data from API endpoint."""
|
"""Fetch data from API endpoint."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway."""
|
"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, override
|
from typing import Any
|
||||||
|
|
||||||
from incomfortclient import Heater as InComfortHeater
|
from incomfortclient import Heater as InComfortHeater
|
||||||
|
|
||||||
@@ -104,13 +104,11 @@ class IncomfortSensor(IncomfortBoilerEntity, SensorEntity):
|
|||||||
self._attr_unique_id = f"{heater.serial_no}_{description.key}"
|
self._attr_unique_id = f"{heater.serial_no}_{description.key}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self._heater.status[self.entity_description.value_key] # type: ignore [no-any-return]
|
return self._heater.status[self.entity_description.value_key] # type: ignore [no-any-return]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return the device state attributes."""
|
"""Return the device state attributes."""
|
||||||
if (extra_key := self.entity_description.extra_key) is None:
|
if (extra_key := self.entity_description.extra_key) is None:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway."""
|
"""Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, override
|
from typing import Any
|
||||||
|
|
||||||
from incomfortclient import Heater as InComfortHeater
|
from incomfortclient import Heater as InComfortHeater
|
||||||
|
|
||||||
@@ -49,13 +49,11 @@ class IncomfortWaterHeater(IncomfortBoilerEntity, WaterHeaterEntity):
|
|||||||
self._attr_unique_id = heater.serial_no
|
self._attr_unique_id = heater.serial_no
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
"""Return the device state attributes."""
|
"""Return the device state attributes."""
|
||||||
return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS}
|
return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
if self._heater.is_tapping:
|
if self._heater.is_tapping:
|
||||||
@@ -69,7 +67,6 @@ class IncomfortWaterHeater(IncomfortBoilerEntity, WaterHeaterEntity):
|
|||||||
return max(self._heater.heater_temp, self._heater.tap_temp)
|
return max(self._heater.heater_temp, self._heater.tap_temp)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
|
||||||
def current_operation(self) -> str | None:
|
def current_operation(self) -> str | None:
|
||||||
"""Return the current operation mode."""
|
"""Return the current operation mode."""
|
||||||
return self._heater.display_text
|
return self._heater.display_text
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["iometer==1.0.1"],
|
"requirements": ["iometer==0.4.0"],
|
||||||
"zeroconf": ["_iometer._tcp.local."]
|
"zeroconf": ["_iometer._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||||||
def async_on_update(self, event: NodeProperty) -> None:
|
def async_on_update(self, event: NodeProperty) -> None:
|
||||||
"""Save brightness in the update event from the ISY Node."""
|
"""Save brightness in the update event from the ISY Node."""
|
||||||
if self._node.status not in (0, ISY_VALUE_UNKNOWN):
|
if self._node.status not in (0, ISY_VALUE_UNKNOWN):
|
||||||
|
self._last_brightness = self._node.status
|
||||||
if self._node.uom == UOM_PERCENTAGE:
|
if self._node.uom == UOM_PERCENTAGE:
|
||||||
self._last_brightness = round(self._node.status * 255.0 / 100.0)
|
self._last_brightness = round(self._node.status * 255.0 / 100.0)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -279,6 +279,10 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
|
|||||||
if uom in (UOM_INDEX, UOM_ON_OFF):
|
if uom in (UOM_INDEX, UOM_ON_OFF):
|
||||||
return cast(str, self.target.formatted)
|
return cast(str, self.target.formatted)
|
||||||
|
|
||||||
|
# Check if this is an index type and get formatted value
|
||||||
|
if uom == UOM_INDEX and hasattr(self.target, "formatted"):
|
||||||
|
return cast(str, self.target.formatted)
|
||||||
|
|
||||||
# Handle ISY precision and rounding
|
# Handle ISY precision and rounding
|
||||||
value = convert_isy_value_to_hass(value, uom, self.target.prec)
|
value = convert_isy_value_to_hass(value, uom, self.target.prec)
|
||||||
if value is None:
|
if value is None:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(device.zones.values())
|
async_add_entities(device.zones.values())
|
||||||
|
|
||||||
# create any components not yet created
|
# create any components not yet created
|
||||||
for controller in (await disco.pi_disco.fetch_controllers()).values():
|
for controller in disco.pi_disco.controllers.values():
|
||||||
init_controller(controller)
|
init_controller(controller)
|
||||||
|
|
||||||
# connect to register any further components
|
# connect to register any further components
|
||||||
|
|||||||
@@ -29,13 +29,12 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
|
|||||||
async with asyncio.timeout(TIMEOUT_DISCOVERY):
|
async with asyncio.timeout(TIMEOUT_DISCOVERY):
|
||||||
await controller_ready.wait()
|
await controller_ready.wait()
|
||||||
|
|
||||||
controllers = await disco.pi_disco.fetch_controllers()
|
if not disco.pi_disco.controllers:
|
||||||
if not controllers:
|
|
||||||
await async_stop_discovery_service(hass)
|
await async_stop_discovery_service(hass)
|
||||||
_LOGGER.debug("No controllers found")
|
_LOGGER.debug("No controllers found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_LOGGER.debug("Controllers %s", controllers)
|
_LOGGER.debug("Controllers %s", disco.pi_disco.controllers)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import datetime
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from random import random
|
from random import random
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.labs import (
|
from homeassistant.components.labs import (
|
||||||
EventLabsUpdatedData,
|
EventLabsUpdatedData,
|
||||||
async_is_preview_feature_enabled,
|
async_is_preview_feature_enabled,
|
||||||
@@ -32,7 +34,7 @@ from homeassistant.const import (
|
|||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
UnitOfVolume,
|
UnitOfVolume,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
from homeassistant.helpers.issue_registry import (
|
from homeassistant.helpers.issue_registry import (
|
||||||
@@ -49,11 +51,9 @@ from homeassistant.util.unit_conversion import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||||
from .services import async_setup_services
|
|
||||||
|
|
||||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
Platform.DEVICE_TRACKER,
|
|
||||||
Platform.FAN,
|
Platform.FAN,
|
||||||
Platform.EVENT,
|
Platform.EVENT,
|
||||||
Platform.IMAGE,
|
Platform.IMAGE,
|
||||||
@@ -69,6 +69,15 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
|||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
SCHEMA_SERVICE_TEST_SERVICE_1 = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("field_1"): vol.Coerce(int),
|
||||||
|
vol.Required("field_2"): vol.In(["off", "auto", "cool"]),
|
||||||
|
vol.Optional("field_3"): vol.Coerce(int),
|
||||||
|
vol.Optional("field_4"): vol.In(["forwards", "reverse"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the demo environment."""
|
"""Set up the demo environment."""
|
||||||
@@ -78,7 +87,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async_setup_services(hass)
|
@callback
|
||||||
|
def service_handler(call: ServiceCall | None = None) -> ServiceResponse:
|
||||||
|
"""Do nothing."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
"test_service_1",
|
||||||
|
service_handler,
|
||||||
|
SCHEMA_SERVICE_TEST_SERVICE_1,
|
||||||
|
description_placeholders={
|
||||||
|
"meep_1": "foo",
|
||||||
|
"meep_2": "bar",
|
||||||
|
"meep_3": "beer",
|
||||||
|
"meep_4": "milk",
|
||||||
|
"meep_5": "https://example.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
"""Demo platform that has a couple of fake device trackers."""
|
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
|
||||||
BaseScannerEntity,
|
|
||||||
SourceType,
|
|
||||||
TrackerEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: ConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the Everything but the Kitchen Sink config entry."""
|
|
||||||
async_add_entities(
|
|
||||||
[
|
|
||||||
DemoTracker(
|
|
||||||
unique_id="kitchen_sink_tracker_001",
|
|
||||||
name="Demo tracker",
|
|
||||||
latitude=hass.config.latitude,
|
|
||||||
longitude=hass.config.longitude,
|
|
||||||
accuracy=10,
|
|
||||||
),
|
|
||||||
DemoScanner(
|
|
||||||
unique_id="kitchen_sink_scanner_001",
|
|
||||||
name="Demo scanner",
|
|
||||||
is_connected=True,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DemoTracker(TrackerEntity):
|
|
||||||
"""Representation of a demo tracker."""
|
|
||||||
|
|
||||||
_attr_should_poll = False
|
|
||||||
_attr_source_type = SourceType.GPS
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
unique_id: str,
|
|
||||||
name: str,
|
|
||||||
latitude: float | None,
|
|
||||||
longitude: float | None,
|
|
||||||
accuracy: float,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the tracker."""
|
|
||||||
self._attr_unique_id = unique_id
|
|
||||||
self._attr_name = name
|
|
||||||
self._attr_latitude = latitude
|
|
||||||
self._attr_longitude = longitude
|
|
||||||
self._attr_location_accuracy = accuracy
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_set_tracker_location(
|
|
||||||
self, latitude: float, longitude: float, accuracy: float
|
|
||||||
) -> None:
|
|
||||||
"""Update the tracker location."""
|
|
||||||
self._attr_latitude = latitude
|
|
||||||
self._attr_longitude = longitude
|
|
||||||
self._attr_location_accuracy = accuracy
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
|
|
||||||
class DemoScanner(BaseScannerEntity):
|
|
||||||
"""Representation of a demo scanner."""
|
|
||||||
|
|
||||||
_attr_should_poll = False
|
|
||||||
_attr_source_type = SourceType.ROUTER
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
unique_id: str,
|
|
||||||
name: str,
|
|
||||||
is_connected: bool,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the scanner."""
|
|
||||||
self._attr_unique_id = unique_id
|
|
||||||
self._attr_name = name
|
|
||||||
self._is_connected = is_connected
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Return true if the device is connected."""
|
|
||||||
return self._is_connected
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_set_scanner_connected(self, connected: bool) -> None:
|
|
||||||
"""Update the scanner connected state."""
|
|
||||||
self._is_connected = connected
|
|
||||||
self.async_write_ha_state()
|
|
||||||
@@ -9,12 +9,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"set_scanner_connected": {
|
|
||||||
"service": "mdi:lan-connect"
|
|
||||||
},
|
|
||||||
"set_tracker_location": {
|
|
||||||
"service": "mdi:map-marker"
|
|
||||||
},
|
|
||||||
"test_service_1": {
|
"test_service_1": {
|
||||||
"sections": {
|
"sections": {
|
||||||
"additional_fields": "mdi:test-tube"
|
"additional_fields": "mdi:test-tube"
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
"""Services for the Everything but the Kitchen Sink integration."""
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
|
|
||||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
|
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse, callback
|
|
||||||
from homeassistant.helpers import config_validation as cv, service
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
SCHEMA_SERVICE_TEST_SERVICE_1 = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required("field_1"): vol.Coerce(int),
|
|
||||||
vol.Required("field_2"): vol.In(["off", "auto", "cool"]),
|
|
||||||
vol.Optional("field_3"): vol.Coerce(int),
|
|
||||||
vol.Optional("field_4"): vol.In(["forward", "reverse"]),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
SERVICE_TEST_SERVICE_1 = "test_service_1"
|
|
||||||
SERVICE_SET_TRACKER_LOCATION = "set_tracker_location"
|
|
||||||
SERVICE_SET_SCANNER_CONNECTED = "set_scanner_connected"
|
|
||||||
|
|
||||||
ATTR_ACCURACY = "accuracy"
|
|
||||||
ATTR_CONNECTED = "connected"
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_setup_services(hass: HomeAssistant) -> None:
|
|
||||||
"""Register services for the Kitchen Sink integration."""
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def service_handler(call: ServiceCall) -> ServiceResponse:
|
|
||||||
"""Do nothing."""
|
|
||||||
return None
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_TEST_SERVICE_1,
|
|
||||||
service_handler,
|
|
||||||
SCHEMA_SERVICE_TEST_SERVICE_1,
|
|
||||||
description_placeholders={
|
|
||||||
"meep_1": "foo",
|
|
||||||
"meep_2": "bar",
|
|
||||||
"meep_3": "beer",
|
|
||||||
"meep_4": "milk",
|
|
||||||
"meep_5": "https://example.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
service.async_register_platform_entity_service(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_SET_TRACKER_LOCATION,
|
|
||||||
entity_domain=DEVICE_TRACKER_DOMAIN,
|
|
||||||
schema={
|
|
||||||
vol.Required(ATTR_LATITUDE): cv.latitude,
|
|
||||||
vol.Required(ATTR_LONGITUDE): cv.longitude,
|
|
||||||
vol.Required(ATTR_ACCURACY): vol.All(vol.Coerce(float), vol.Range(min=0)),
|
|
||||||
},
|
|
||||||
func="async_set_tracker_location",
|
|
||||||
)
|
|
||||||
|
|
||||||
service.async_register_platform_entity_service(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_SET_SCANNER_CONNECTED,
|
|
||||||
entity_domain=DEVICE_TRACKER_DOMAIN,
|
|
||||||
schema={vol.Required(ATTR_CONNECTED): cv.boolean},
|
|
||||||
func="async_set_scanner_connected",
|
|
||||||
)
|
|
||||||
@@ -30,44 +30,3 @@ test_service_1:
|
|||||||
options:
|
options:
|
||||||
- "forward"
|
- "forward"
|
||||||
- "reverse"
|
- "reverse"
|
||||||
set_tracker_location:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
integration: kitchen_sink
|
|
||||||
domain: device_tracker
|
|
||||||
fields:
|
|
||||||
latitude:
|
|
||||||
required: true
|
|
||||||
example: 52.379189
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: -90
|
|
||||||
max: 90
|
|
||||||
step: any
|
|
||||||
longitude:
|
|
||||||
required: true
|
|
||||||
example: 4.899431
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: -180
|
|
||||||
max: 180
|
|
||||||
step: any
|
|
||||||
accuracy:
|
|
||||||
required: true
|
|
||||||
example: 10
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: 0
|
|
||||||
max: 10000
|
|
||||||
unit_of_measurement: m
|
|
||||||
set_scanner_connected:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
integration: kitchen_sink
|
|
||||||
domain: device_tracker
|
|
||||||
fields:
|
|
||||||
connected:
|
|
||||||
required: true
|
|
||||||
example: true
|
|
||||||
selector:
|
|
||||||
boolean:
|
|
||||||
|
|||||||
@@ -135,34 +135,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"set_scanner_connected": {
|
|
||||||
"description": "Sets the connected state of a demo scanner entity.",
|
|
||||||
"fields": {
|
|
||||||
"connected": {
|
|
||||||
"description": "Whether the device should be reported as connected.",
|
|
||||||
"name": "Connected"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Set scanner connected"
|
|
||||||
},
|
|
||||||
"set_tracker_location": {
|
|
||||||
"description": "Sets the location and accuracy of a demo tracker entity.",
|
|
||||||
"fields": {
|
|
||||||
"accuracy": {
|
|
||||||
"description": "Location accuracy in meters.",
|
|
||||||
"name": "Accuracy"
|
|
||||||
},
|
|
||||||
"latitude": {
|
|
||||||
"description": "Latitude of the new location.",
|
|
||||||
"name": "Latitude"
|
|
||||||
},
|
|
||||||
"longitude": {
|
|
||||||
"description": "Longitude of the new location.",
|
|
||||||
"name": "Longitude"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Set tracker location"
|
|
||||||
},
|
|
||||||
"test_service_1": {
|
"test_service_1": {
|
||||||
"description": "Fake action for testing {meep_2}",
|
"description": "Fake action for testing {meep_2}",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
behavior:
|
behavior:
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ class ThinQEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]):
|
|||||||
except ThinQAPIException as exc:
|
except ThinQAPIException as exc:
|
||||||
if on_fail_method:
|
if on_fail_method:
|
||||||
on_fail_method()
|
on_fail_method()
|
||||||
raise ServiceValidationError(exc.message) from exc
|
# pylint: disable-next=home-assistant-exception-message-with-translation
|
||||||
|
raise ServiceValidationError(
|
||||||
|
exc.message, translation_domain=DOMAIN, translation_key=exc.code
|
||||||
|
) from exc
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
if on_fail_method:
|
if on_fail_method:
|
||||||
on_fail_method()
|
on_fail_method()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
fields:
|
fields:
|
||||||
behavior: &trigger_behavior
|
behavior: &trigger_behavior
|
||||||
required: true
|
required: true
|
||||||
default: each
|
default: any
|
||||||
selector:
|
selector:
|
||||||
automation_behavior:
|
automation_behavior:
|
||||||
mode: trigger
|
mode: trigger
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user