mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Merge branch 'dev' into mill
This commit is contained in:
commit
72c9fe9a92
@ -62,7 +62,7 @@
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||
"url": "./script/json_schemas/manifest_schema.json"
|
||||
"url": "${containerWorkspaceFolder}/script/json_schemas/manifest_schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
11
.gitattributes
vendored
11
.gitattributes
vendored
@ -11,3 +11,14 @@
|
||||
*.pcm binary
|
||||
|
||||
Dockerfile.dev linguist-language=Dockerfile
|
||||
|
||||
# Generated files
|
||||
CODEOWNERS linguist-generated=true
|
||||
Dockerfile linguist-generated=true
|
||||
homeassistant/generated/*.py linguist-generated=true
|
||||
mypy.ini linguist-generated=true
|
||||
requirements.txt linguist-generated=true
|
||||
requirements_all.txt linguist-generated=true
|
||||
requirements_test_all.txt linguist-generated=true
|
||||
requirements_test_pre_commit.txt linguist-generated=true
|
||||
script/hassfest/docker/Dockerfile linguist-generated=true
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -46,6 +46,8 @@
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to frontend pull request:
|
||||
|
||||
## Checklist
|
||||
<!--
|
||||
|
BIN
.github/assets/screenshot-integrations.png
vendored
BIN
.github/assets/screenshot-integrations.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 99 KiB |
100
.github/copilot-instructions.md
vendored
Normal file
100
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
# Instructions for GitHub Copilot
|
||||
|
||||
This repository holds the core of Home Assistant, a Python 3 based home
|
||||
automation application.
|
||||
|
||||
- Python code must be compatible with Python 3.13
|
||||
- Use the newest Python language features if possible:
|
||||
- Pattern matching
|
||||
- Type hints
|
||||
- f-strings for string formatting over `%` or `.format()`
|
||||
- Dataclasses
|
||||
- Walrus operator
|
||||
- Code quality tools:
|
||||
- Formatting: Ruff
|
||||
- Linting: PyLint and Ruff
|
||||
- Type checking: MyPy
|
||||
- Testing: pytest with plain functions and fixtures
|
||||
- Inline code documentation:
|
||||
- File headers should be short and concise:
|
||||
```python
|
||||
"""Integration for Peblar EV chargers."""
|
||||
```
|
||||
- Every method and function needs a docstring:
|
||||
```python
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool:
|
||||
"""Set up Peblar from a config entry."""
|
||||
...
|
||||
```
|
||||
- All code and comments and other text are written in American English
|
||||
- Follow existing code style patterns as much as possible
|
||||
- Core locations:
|
||||
- Shared constants: `homeassistant/const.py`, use them instead of hardcoding
|
||||
strings or creating duplicate integration constants.
|
||||
- Integration files:
|
||||
- Constants: `homeassistant/components/{domain}/const.py`
|
||||
- Models: `homeassistant/components/{domain}/models.py`
|
||||
- Coordinator: `homeassistant/components/{domain}/coordinator.py`
|
||||
- Config flow: `homeassistant/components/{domain}/config_flow.py`
|
||||
- Platform code: `homeassistant/components/{domain}/{platform}.py`
|
||||
- All external I/O operations must be async
|
||||
- Async patterns:
|
||||
- Avoid sleeping in loops
|
||||
- Avoid awaiting in loops, gather instead
|
||||
- No blocking calls
|
||||
- Polling:
|
||||
- Follow update coordinator pattern, when possible
|
||||
- Polling interval may not be configurable by the user
|
||||
- For local network polling, the minimum interval is 5 seconds
|
||||
- For cloud polling, the minimum interval is 60 seconds
|
||||
- Error handling:
|
||||
- Use specific exceptions from `homeassistant.exceptions`
|
||||
- Setup failures:
|
||||
- Temporary: Raise `ConfigEntryNotReady`
|
||||
- Permanent: Use `ConfigEntryError`
|
||||
- Logging:
|
||||
- Message format:
|
||||
- No periods at end
|
||||
- No integration names or domains (added automatically)
|
||||
- No sensitive data (keys, tokens, passwords), even when those are incorrect.
|
||||
- Be very restrictive on the use of logging info messages, use debug for
|
||||
anything which is not targeting the user.
|
||||
- Use lazy logging (no f-strings):
|
||||
```python
|
||||
_LOGGER.debug("This is a log message with %s", variable)
|
||||
```
|
||||
- Entities:
|
||||
- Ensure unique IDs for state persistence:
|
||||
- Unique IDs should not contain values that are subject to user or network change.
|
||||
- An ID needs to be unique per platform, not per integration.
|
||||
- The ID does not have to contain the integration domain or platform.
|
||||
- Acceptable examples:
|
||||
- Serial number of a device
|
||||
- MAC address of a device formatted using `homeassistant.helpers.device_registry.format_mac`
|
||||
Do not obtain the MAC address through arp cache of local network access,
|
||||
only use the MAC address provided by discovery or the device itself.
|
||||
- Unique identifier that is physically printed on the device or burned into an EEPROM
|
||||
- Not acceptable examples:
|
||||
- IP Address
|
||||
- Device name
|
||||
- Hostname
|
||||
- URL
|
||||
- Email address
|
||||
- Username
|
||||
- For entities that are setup by a config entry, the config entry ID
|
||||
can be used as a last resort if no other Unique ID is available.
|
||||
For example: `f"{entry.entry_id}-battery"`
|
||||
- If the state value is unknown, use `None`
|
||||
- Do not use the `unavailable` string as a state value,
|
||||
implement the `available()` property method instead
|
||||
- Do not use the `unknown` string as a state value, use `None` instead
|
||||
- Extra entity state attributes:
|
||||
- The keys of all state attributes should always be present
|
||||
- If the value is unknown, use `None`
|
||||
- Provide descriptive state attributes
|
||||
- Testing:
|
||||
- Test location: `tests/components/{domain}/`
|
||||
- Use pytest fixtures from `tests.common`
|
||||
- Mock external dependencies
|
||||
- Use snapshots for complex data
|
||||
- Follow existing test patterns
|
44
.github/workflows/builder.yml
vendored
44
.github/workflows/builder.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@ -69,7 +69,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@ -197,7 +197,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2024.08.2
|
||||
uses: home-assistant/builder@2025.02.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -263,7 +263,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2024.08.2
|
||||
uses: home-assistant/builder@2025.02.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -324,7 +324,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.7.0
|
||||
uses: sigstore/cosign-installer@v3.8.1
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
@ -448,18 +448,21 @@ jobs:
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@ -473,16 +476,13 @@ jobs:
|
||||
run: |
|
||||
# Remove dist, build, and homeassistant.egg-info
|
||||
# when build locally for testing!
|
||||
pip install twine build
|
||||
pip install build
|
||||
python -m build
|
||||
|
||||
- name: Upload package
|
||||
shell: bash
|
||||
run: |
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
|
||||
twine upload dist/* --skip-existing
|
||||
- name: Upload package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
hassfest-image:
|
||||
name: Build and test hassfest image
|
||||
@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@ -531,7 +531,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
237
.github/workflows/ci.yaml
vendored
237
.github/workflows/ci.yaml
vendored
@ -40,9 +40,9 @@ env:
|
||||
CACHE_VERSION: 11
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.1"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
|
||||
HA_SHORT_VERSION: "2025.4"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
@ -89,6 +89,7 @@ jobs:
|
||||
test_groups: ${{ steps.info.outputs.test_groups }}
|
||||
tests_glob: ${{ steps.info.outputs.tests_glob }}
|
||||
tests: ${{ steps.info.outputs.tests }}
|
||||
lint_only: ${{ steps.info.outputs.lint_only }}
|
||||
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
@ -142,6 +143,7 @@ jobs:
|
||||
test_group_count=10
|
||||
tests="[]"
|
||||
tests_glob=""
|
||||
lint_only=""
|
||||
skip_coverage=""
|
||||
|
||||
if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]];
|
||||
@ -192,6 +194,17 @@ jobs:
|
||||
test_full_suite="true"
|
||||
fi
|
||||
|
||||
if [[ "${{ github.event.inputs.lint-only }}" == "true" ]] \
|
||||
|| [[ "${{ github.event.inputs.pylint-only }}" == "true" ]] \
|
||||
|| [[ "${{ github.event.inputs.mypy-only }}" == "true" ]] \
|
||||
|| [[ "${{ github.event.inputs.audit-licenses-only }}" == "true" ]] \
|
||||
|| [[ "${{ github.event_name }}" == "push" \
|
||||
&& "${{ github.event.repository.full_name }}" != "home-assistant/core" ]];
|
||||
then
|
||||
lint_only="true"
|
||||
skip_coverage="true"
|
||||
fi
|
||||
|
||||
if [[ "${{ github.event.inputs.skip-coverage }}" == "true" ]] \
|
||||
|| [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-skip-coverage') }}" == "true" ]];
|
||||
then
|
||||
@ -217,6 +230,8 @@ jobs:
|
||||
echo "tests=${tests}" >> $GITHUB_OUTPUT
|
||||
echo "tests_glob: ${tests_glob}"
|
||||
echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT
|
||||
echo "lint_only": ${lint_only}
|
||||
echo "lint_only=${lint_only}" >> $GITHUB_OUTPUT
|
||||
echo "skip_coverage: ${skip_coverage}"
|
||||
echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT
|
||||
|
||||
@ -234,13 +249,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -256,7 +271,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@ -279,14 +294,14 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -295,7 +310,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@ -319,14 +334,14 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -335,7 +350,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@ -359,14 +374,14 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -375,7 +390,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@ -469,7 +484,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@ -482,7 +497,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@ -490,7 +505,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.2
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@ -537,7 +552,7 @@ jobs:
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
- name: Upload pip_freeze artifact
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: pip-freeze-${{ matrix.python-version }}
|
||||
path: pip_freeze.txt
|
||||
@ -572,13 +587,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -605,13 +620,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -623,6 +638,25 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.gen_requirements_all validate
|
||||
|
||||
dependency-review:
|
||||
name: Dependency review
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& needs.info.outputs.requirements == 'true'
|
||||
&& github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4.5.0
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
audit-licenses:
|
||||
name: Audit licenses
|
||||
runs-on: ubuntu-24.04
|
||||
@ -643,13 +677,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -661,7 +695,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
|
||||
- name: Upload licenses
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||
path: licenses-${{ matrix.python-version }}.json
|
||||
@ -686,13 +720,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -733,13 +767,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -778,7 +812,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -791,7 +825,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -799,7 +833,7 @@ jobs:
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.2
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@ -829,11 +863,7 @@ jobs:
|
||||
prepare-pytest-full:
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
&& github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
needs.info.outputs.lint_only != 'true'
|
||||
&& needs.info.outputs.test_full_suite == 'true'
|
||||
needs:
|
||||
- info
|
||||
@ -859,13 +889,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -877,7 +907,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: pytest_buckets
|
||||
path: pytest_buckets.txt
|
||||
@ -886,11 +916,7 @@ jobs:
|
||||
pytest-full:
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
&& github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
needs.info.outputs.lint_only != 'true'
|
||||
&& needs.info.outputs.test_full_suite == 'true'
|
||||
needs:
|
||||
- info
|
||||
@ -923,13 +949,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -942,7 +968,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@ -962,6 +988,7 @@ jobs:
|
||||
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
|
||||
cov_params+=(--cov="homeassistant")
|
||||
cov_params+=(--cov-report=xml)
|
||||
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
|
||||
fi
|
||||
|
||||
echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)"
|
||||
@ -975,22 +1002,29 @@ jobs:
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
-p no:sugar \
|
||||
--exclude-warning-annotations \
|
||||
$(sed -n "${{ matrix.group }},1p" pytest_buckets.txt) \
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
- name: Remove pytest_buckets
|
||||
run: rm pytest_buckets.txt
|
||||
- name: Check dirty
|
||||
@ -1008,11 +1042,7 @@ jobs:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
options: --health-cmd="mysqladmin ping -uroot -ppassword" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
&& github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
needs.info.outputs.lint_only != 'true'
|
||||
&& needs.info.outputs.mariadb_groups != '[]'
|
||||
needs:
|
||||
- info
|
||||
@ -1044,13 +1074,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -1087,6 +1117,7 @@ jobs:
|
||||
cov_params+=(--cov="homeassistant.components.recorder")
|
||||
cov_params+=(--cov-report=xml)
|
||||
cov_params+=(--cov-report=term-missing)
|
||||
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
|
||||
fi
|
||||
|
||||
python3 -b -X dev -m pytest \
|
||||
@ -1098,6 +1129,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
--durations=10 \
|
||||
-p no:sugar \
|
||||
--exclude-warning-annotations \
|
||||
--dburl=mysql://root:password@127.0.0.1/homeassistant-test \
|
||||
tests/components/history \
|
||||
tests/components/logbook \
|
||||
@ -1106,7 +1138,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@ -1114,12 +1146,19 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: test-results-mariadb-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
path: junit.xml
|
||||
- name: Check dirty
|
||||
run: |
|
||||
./script/check_dirty
|
||||
@ -1135,11 +1174,7 @@ jobs:
|
||||
POSTGRES_PASSWORD: password
|
||||
options: --health-cmd="pg_isready -hlocalhost -Upostgres" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
&& github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
needs.info.outputs.lint_only != 'true'
|
||||
&& needs.info.outputs.postgresql_groups != '[]'
|
||||
needs:
|
||||
- info
|
||||
@ -1173,13 +1208,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -1216,6 +1251,7 @@ jobs:
|
||||
cov_params+=(--cov="homeassistant.components.recorder")
|
||||
cov_params+=(--cov-report=xml)
|
||||
cov_params+=(--cov-report=term-missing)
|
||||
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
|
||||
fi
|
||||
|
||||
python3 -b -X dev -m pytest \
|
||||
@ -1228,6 +1264,7 @@ jobs:
|
||||
--durations=0 \
|
||||
--durations-min=10 \
|
||||
-p no:sugar \
|
||||
--exclude-warning-annotations \
|
||||
--dburl=postgresql://postgres:password@127.0.0.1/homeassistant-test \
|
||||
tests/components/history \
|
||||
tests/components/logbook \
|
||||
@ -1236,7 +1273,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@ -1244,12 +1281,19 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: test-results-postgres-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
path: junit.xml
|
||||
- name: Check dirty
|
||||
run: |
|
||||
./script/check_dirty
|
||||
@ -1268,12 +1312,12 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v5.1.1
|
||||
uses: codecov/codecov-action@v5.4.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@ -1282,11 +1326,7 @@ jobs:
|
||||
pytest-partial:
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
&& github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
needs.info.outputs.lint_only != 'true'
|
||||
&& needs.info.outputs.tests_glob
|
||||
&& needs.info.outputs.test_full_suite == 'false'
|
||||
needs:
|
||||
@ -1319,13 +1359,13 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
uses: actions/cache/restore@v4.2.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@ -1362,6 +1402,7 @@ jobs:
|
||||
cov_params+=(--cov="homeassistant.components.${{ matrix.group }}")
|
||||
cov_params+=(--cov-report=xml)
|
||||
cov_params+=(--cov-report=term-missing)
|
||||
cov_params+=(--junitxml=junit.xml -o junit_family=legacy)
|
||||
fi
|
||||
|
||||
python3 -b -X dev -m pytest \
|
||||
@ -1374,22 +1415,29 @@ jobs:
|
||||
--durations=0 \
|
||||
--durations-min=1 \
|
||||
-p no:sugar \
|
||||
--exclude-warning-annotations \
|
||||
tests/components/${{ matrix.group }} \
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: pytest-*.txt
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: junit.xml
|
||||
- name: Check dirty
|
||||
run: |
|
||||
./script/check_dirty
|
||||
@ -1406,12 +1454,37 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v5.1.1
|
||||
uses: codecov/codecov-action@v5.4.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
upload-test-results:
|
||||
name: Upload test results to Codecov
|
||||
# codecov/test-results-action currently doesn't support tokenless uploads
|
||||
# therefore we can't run it on forks
|
||||
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) && needs.info.outputs.skip_coverage != 'true' && !cancelled() }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- info
|
||||
- pytest-partial
|
||||
- pytest-full
|
||||
- pytest-postgres
|
||||
- pytest-mariadb
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.27.9
|
||||
uses: github/codeql-action/init@v3.28.11
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.27.9
|
||||
uses: github/codeql-action/analyze@v3.28.11
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
# - No PRs marked as no-stale
|
||||
# - No issues (-1)
|
||||
- name: 60 days stale PRs policy
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 60
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: 90 days stale issues
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
days-before-stale: 90
|
||||
@ -87,7 +87,7 @@ jobs:
|
||||
# - No Issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: Needs more information stale issues policy
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
only-labels: "needs-more-information"
|
||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@ -10,7 +10,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
101
.github/workflows/wheels.yml
vendored
101
.github/workflows/wheels.yml
vendored
@ -17,7 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -76,18 +76,37 @@ jobs:
|
||||
|
||||
# Use C-Extension for SQLAlchemy
|
||||
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
||||
|
||||
# Add additional pip wheel build constraints
|
||||
echo "PIP_CONSTRAINT=build_constraints.txt"
|
||||
) > .env_file
|
||||
|
||||
- name: Write pip wheel build constraints
|
||||
run: |
|
||||
(
|
||||
# ninja 1.11.1.2 + 1.11.1.3 seem to be broken on at least armhf
|
||||
# this caused the numpy builds to fail
|
||||
# https://github.com/scikit-build/ninja-python-distributions/issues/274
|
||||
echo "ninja==1.11.1.1"
|
||||
) > build_constraints.txt
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
include-hidden-files: true
|
||||
overwrite: true
|
||||
|
||||
- name: Upload build_constraints
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: build_constraints
|
||||
path: ./build_constraints.txt
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@ -99,7 +118,7 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
@ -112,19 +131,24 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp312", "cp313"]
|
||||
abi: ["cp313"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@ -135,14 +159,14 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2024.11.0
|
||||
uses: home-assistant/wheels@2025.02.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
|
||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
|
||||
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
@ -156,24 +180,29 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp312", "cp313"]
|
||||
abi: ["cp313"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
uses: actions/download-artifact@v4.1.9
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
@ -189,52 +218,16 @@ jobs:
|
||||
sed -i "/uv/d" requirements.txt
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Split requirements all
|
||||
run: |
|
||||
# We split requirements all into multiple files.
|
||||
# This is to prevent the build from running out of memory when
|
||||
# resolving packages on 32-bits systems (like armhf, armv7).
|
||||
|
||||
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt
|
||||
|
||||
- name: Build wheels (part 1)
|
||||
uses: home-assistant/wheels@2024.11.0
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.02.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtaa"
|
||||
|
||||
- name: Build wheels (part 2)
|
||||
uses: home-assistant/wheels@2024.11.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtab"
|
||||
|
||||
- name: Build wheels (part 3)
|
||||
uses: home-assistant/wheels@2024.11.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtac"
|
||||
requirements: "requirements_all.txt"
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -69,6 +69,7 @@ test-reports/
|
||||
test-results.xml
|
||||
test-output.xml
|
||||
pytest-*.txt
|
||||
junit.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.3
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
@ -8,11 +8,11 @@ repos:
|
||||
- id: ruff-format
|
||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
||||
- --ignore-words-list=aiport,astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
|
||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||
- --quiet-level=2
|
||||
exclude_types: [csv, json, html]
|
||||
@ -61,13 +61,14 @@ repos:
|
||||
name: mypy
|
||||
entry: script/run-in-env.sh mypy
|
||||
language: script
|
||||
types_or: [python, pyi]
|
||||
require_serial: true
|
||||
types_or: [python, pyi]
|
||||
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
|
||||
entry: script/run-in-env.sh pylint --ignore-missing-annotations=y
|
||||
language: script
|
||||
require_serial: true
|
||||
types_or: [python, pyi]
|
||||
files: ^(homeassistant|tests)/.+\.(py|pyi)$
|
||||
- id: gen_requirements_all
|
||||
|
@ -103,6 +103,7 @@ homeassistant.components.auth.*
|
||||
homeassistant.components.automation.*
|
||||
homeassistant.components.awair.*
|
||||
homeassistant.components.axis.*
|
||||
homeassistant.components.azure_storage.*
|
||||
homeassistant.components.backup.*
|
||||
homeassistant.components.baf.*
|
||||
homeassistant.components.bang_olufsen.*
|
||||
@ -119,6 +120,7 @@ homeassistant.components.bluetooth_tracker.*
|
||||
homeassistant.components.bmw_connected_drive.*
|
||||
homeassistant.components.bond.*
|
||||
homeassistant.components.braviatv.*
|
||||
homeassistant.components.bring.*
|
||||
homeassistant.components.brother.*
|
||||
homeassistant.components.browser.*
|
||||
homeassistant.components.bryant_evolution.*
|
||||
@ -134,6 +136,7 @@ homeassistant.components.clicksend.*
|
||||
homeassistant.components.climate.*
|
||||
homeassistant.components.cloud.*
|
||||
homeassistant.components.co2signal.*
|
||||
homeassistant.components.comelit.*
|
||||
homeassistant.components.command_line.*
|
||||
homeassistant.components.config.*
|
||||
homeassistant.components.configurator.*
|
||||
@ -217,6 +220,7 @@ homeassistant.components.goalzero.*
|
||||
homeassistant.components.google.*
|
||||
homeassistant.components.google_assistant_sdk.*
|
||||
homeassistant.components.google_cloud.*
|
||||
homeassistant.components.google_drive.*
|
||||
homeassistant.components.google_photos.*
|
||||
homeassistant.components.google_sheets.*
|
||||
homeassistant.components.govee_ble.*
|
||||
@ -224,18 +228,22 @@ homeassistant.components.gpsd.*
|
||||
homeassistant.components.greeneye_monitor.*
|
||||
homeassistant.components.group.*
|
||||
homeassistant.components.guardian.*
|
||||
homeassistant.components.habitica.*
|
||||
homeassistant.components.hardkernel.*
|
||||
homeassistant.components.hardware.*
|
||||
homeassistant.components.heos.*
|
||||
homeassistant.components.here_travel_time.*
|
||||
homeassistant.components.history.*
|
||||
homeassistant.components.history_stats.*
|
||||
homeassistant.components.holiday.*
|
||||
homeassistant.components.home_connect.*
|
||||
homeassistant.components.homeassistant.*
|
||||
homeassistant.components.homeassistant_alerts.*
|
||||
homeassistant.components.homeassistant_green.*
|
||||
homeassistant.components.homeassistant_hardware.*
|
||||
homeassistant.components.homeassistant_sky_connect.*
|
||||
homeassistant.components.homeassistant_yellow.*
|
||||
homeassistant.components.homee.*
|
||||
homeassistant.components.homekit.*
|
||||
homeassistant.components.homekit_controller
|
||||
homeassistant.components.homekit_controller.alarm_control_panel
|
||||
@ -261,6 +269,7 @@ homeassistant.components.image_processing.*
|
||||
homeassistant.components.image_upload.*
|
||||
homeassistant.components.imap.*
|
||||
homeassistant.components.imgw_pib.*
|
||||
homeassistant.components.incomfort.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.input_text.*
|
||||
@ -291,6 +300,7 @@ homeassistant.components.lcn.*
|
||||
homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.led_ble.*
|
||||
homeassistant.components.lektrico.*
|
||||
homeassistant.components.letpot.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
homeassistant.components.light.*
|
||||
@ -305,12 +315,16 @@ homeassistant.components.logbook.*
|
||||
homeassistant.components.logger.*
|
||||
homeassistant.components.london_underground.*
|
||||
homeassistant.components.lookin.*
|
||||
homeassistant.components.lovelace.*
|
||||
homeassistant.components.luftdaten.*
|
||||
homeassistant.components.madvr.*
|
||||
homeassistant.components.manual.*
|
||||
homeassistant.components.mastodon.*
|
||||
homeassistant.components.matrix.*
|
||||
homeassistant.components.matter.*
|
||||
homeassistant.components.mcp.*
|
||||
homeassistant.components.mcp_server.*
|
||||
homeassistant.components.mealie.*
|
||||
homeassistant.components.media_extractor.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.media_source.*
|
||||
@ -351,6 +365,7 @@ homeassistant.components.number.*
|
||||
homeassistant.components.nut.*
|
||||
homeassistant.components.onboarding.*
|
||||
homeassistant.components.oncue.*
|
||||
homeassistant.components.onedrive.*
|
||||
homeassistant.components.onewire.*
|
||||
homeassistant.components.onkyo.*
|
||||
homeassistant.components.open_meteo.*
|
||||
@ -361,10 +376,14 @@ homeassistant.components.openuv.*
|
||||
homeassistant.components.oralb.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.overkiz.*
|
||||
homeassistant.components.overseerr.*
|
||||
homeassistant.components.p1_monitor.*
|
||||
homeassistant.components.pandora.*
|
||||
homeassistant.components.panel_custom.*
|
||||
homeassistant.components.peblar.*
|
||||
homeassistant.components.peco.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.person.*
|
||||
homeassistant.components.pi_hole.*
|
||||
homeassistant.components.ping.*
|
||||
homeassistant.components.plugwise.*
|
||||
@ -378,6 +397,9 @@ homeassistant.components.pure_energie.*
|
||||
homeassistant.components.purpleair.*
|
||||
homeassistant.components.pushbullet.*
|
||||
homeassistant.components.pvoutput.*
|
||||
homeassistant.components.pyload.*
|
||||
homeassistant.components.python_script.*
|
||||
homeassistant.components.qbus.*
|
||||
homeassistant.components.qnap_qsw.*
|
||||
homeassistant.components.rabbitair.*
|
||||
homeassistant.components.radarr.*
|
||||
@ -388,6 +410,7 @@ homeassistant.components.raspberry_pi.*
|
||||
homeassistant.components.rdw.*
|
||||
homeassistant.components.recollect_waste.*
|
||||
homeassistant.components.recorder.*
|
||||
homeassistant.components.remember_the_milk.*
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.renault.*
|
||||
homeassistant.components.reolink.*
|
||||
@ -419,6 +442,7 @@ homeassistant.components.select.*
|
||||
homeassistant.components.sensibo.*
|
||||
homeassistant.components.sensirion_ble.*
|
||||
homeassistant.components.sensor.*
|
||||
homeassistant.components.sensorpush_cloud.*
|
||||
homeassistant.components.sensoterra.*
|
||||
homeassistant.components.senz.*
|
||||
homeassistant.components.sfr_box.*
|
||||
@ -506,6 +530,7 @@ homeassistant.components.vallox.*
|
||||
homeassistant.components.valve.*
|
||||
homeassistant.components.velbus.*
|
||||
homeassistant.components.vlc_telnet.*
|
||||
homeassistant.components.vodafone_station.*
|
||||
homeassistant.components.wake_on_lan.*
|
||||
homeassistant.components.wake_word.*
|
||||
homeassistant.components.wallbox.*
|
||||
|
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@ -38,10 +38,17 @@
|
||||
"module": "pytest",
|
||||
"justMyCode": false,
|
||||
"args": [
|
||||
"--timeout=10",
|
||||
"--picked"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Home Assistant: Debug Current Test File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "pytest",
|
||||
"console": "integratedTerminal",
|
||||
"args": ["-vv", "${file}"]
|
||||
},
|
||||
{
|
||||
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
||||
// See https://www.home-assistant.io/integrations/debugpy/
|
||||
@ -77,4 +84,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
3
.vscode/settings.default.json
vendored
3
.vscode/settings.default.json
vendored
@ -1,5 +1,5 @@
|
||||
{
|
||||
// Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json
|
||||
// Please keep this file (mostly!) in sync with settings in home-assistant/.devcontainer/devcontainer.json
|
||||
// Added --no-cov to work around TypeError: message must be set
|
||||
// https://github.com/microsoft/vscode-python/issues/14067
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
@ -12,6 +12,7 @@
|
||||
"fileMatch": [
|
||||
"homeassistant/components/*/manifest.json"
|
||||
],
|
||||
// This value differs between working with devcontainer and locally, therefor this value should NOT be in sync!
|
||||
"url": "./script/json_schemas/manifest_schema.json"
|
||||
}
|
||||
]
|
||||
|
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@ -148,7 +148,7 @@
|
||||
{
|
||||
"label": "Install all Test Requirements",
|
||||
"type": "shell",
|
||||
"command": "uv pip install -r requirements_test_all.txt",
|
||||
"command": "uv pip install -r requirements.txt -r requirements_test_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
111
CODEOWNERS
generated
111
CODEOWNERS
generated
@ -180,6 +180,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/azure_event_hub/ @eavanvalkenburg
|
||||
/tests/components/azure_event_hub/ @eavanvalkenburg
|
||||
/homeassistant/components/azure_service_bus/ @hfurubotten
|
||||
/homeassistant/components/azure_storage/ @zweckj
|
||||
/tests/components/azure_storage/ @zweckj
|
||||
/homeassistant/components/backup/ @home-assistant/core
|
||||
/tests/components/backup/ @home-assistant/core
|
||||
/homeassistant/components/baf/ @bdraco @jfroy
|
||||
@ -566,6 +568,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/google_assistant_sdk/ @tronikos
|
||||
/homeassistant/components/google_cloud/ @lufton @tronikos
|
||||
/tests/components/google_cloud/ @lufton @tronikos
|
||||
/homeassistant/components/google_drive/ @tronikos
|
||||
/tests/components/google_drive/ @tronikos
|
||||
/homeassistant/components/google_generative_ai_conversation/ @tronikos
|
||||
/tests/components/google_generative_ai_conversation/ @tronikos
|
||||
/homeassistant/components/google_mail/ @tkdrob
|
||||
@ -578,8 +582,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/google_tasks/ @allenporter
|
||||
/homeassistant/components/google_travel_time/ @eifinger
|
||||
/tests/components/google_travel_time/ @eifinger
|
||||
/homeassistant/components/govee_ble/ @bdraco @PierreAronnax
|
||||
/tests/components/govee_ble/ @bdraco @PierreAronnax
|
||||
/homeassistant/components/govee_ble/ @bdraco
|
||||
/tests/components/govee_ble/ @bdraco
|
||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||
/tests/components/govee_light_local/ @Galorhallen
|
||||
/homeassistant/components/gpsd/ @fabaff @jrieger
|
||||
@ -623,8 +627,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/hlk_sw16/ @jameshilliard
|
||||
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
|
||||
/tests/components/holiday/ @jrieger @gjohansson-ST
|
||||
/homeassistant/components/home_connect/ @DavidMStraub @Diegorro98
|
||||
/tests/components/home_connect/ @DavidMStraub @Diegorro98
|
||||
/homeassistant/components/home_connect/ @DavidMStraub @Diegorro98 @MartinHjelmare
|
||||
/tests/components/home_connect/ @DavidMStraub @Diegorro98 @MartinHjelmare
|
||||
/homeassistant/components/homeassistant/ @home-assistant/core
|
||||
/tests/components/homeassistant/ @home-assistant/core
|
||||
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
||||
@ -637,6 +641,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homeassistant_sky_connect/ @home-assistant/core
|
||||
/homeassistant/components/homeassistant_yellow/ @home-assistant/core
|
||||
/tests/components/homeassistant_yellow/ @home-assistant/core
|
||||
/homeassistant/components/homee/ @Taraman17
|
||||
/tests/components/homee/ @Taraman17
|
||||
/homeassistant/components/homekit/ @bdraco
|
||||
/tests/components/homekit/ @bdraco
|
||||
/homeassistant/components/homekit_controller/ @Jc2k @bdraco
|
||||
@ -680,12 +686,12 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/iammeter/ @lewei50
|
||||
/homeassistant/components/iaqualink/ @flz
|
||||
/tests/components/iaqualink/ @flz
|
||||
/homeassistant/components/ibeacon/ @bdraco
|
||||
/tests/components/ibeacon/ @bdraco
|
||||
/homeassistant/components/icloud/ @Quentame @nzapponi
|
||||
/tests/components/icloud/ @Quentame @nzapponi
|
||||
/homeassistant/components/idasen_desk/ @abmantis
|
||||
/tests/components/idasen_desk/ @abmantis
|
||||
/homeassistant/components/igloohome/ @keithle888
|
||||
/tests/components/igloohome/ @keithle888
|
||||
/homeassistant/components/ign_sismologia/ @exxamalte
|
||||
/tests/components/ign_sismologia/ @exxamalte
|
||||
/homeassistant/components/image/ @home-assistant/core
|
||||
@ -727,6 +733,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam
|
||||
/tests/components/intent/ @home-assistant/core @synesthesiam
|
||||
/homeassistant/components/intesishome/ @jnimmo
|
||||
/homeassistant/components/iometer/ @MaestroOnICe
|
||||
/tests/components/iometer/ @MaestroOnICe
|
||||
/homeassistant/components/ios/ @robbiet480
|
||||
/tests/components/ios/ @robbiet480
|
||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||
@ -761,8 +769,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ituran/ @shmuelzon
|
||||
/homeassistant/components/izone/ @Swamp-Ig
|
||||
/tests/components/izone/ @Swamp-Ig
|
||||
/homeassistant/components/jellyfin/ @j-stienstra @ctalkington
|
||||
/tests/components/jellyfin/ @j-stienstra @ctalkington
|
||||
/homeassistant/components/jellyfin/ @RunC0deRun @ctalkington
|
||||
/tests/components/jellyfin/ @RunC0deRun @ctalkington
|
||||
/homeassistant/components/jewish_calendar/ @tsvi
|
||||
/tests/components/jewish_calendar/ @tsvi
|
||||
/homeassistant/components/juicenet/ @jesserockz
|
||||
@ -827,6 +835,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/led_ble/ @bdraco
|
||||
/homeassistant/components/lektrico/ @lektrico
|
||||
/tests/components/lektrico/ @lektrico
|
||||
/homeassistant/components/letpot/ @jpelgrom
|
||||
/tests/components/letpot/ @jpelgrom
|
||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
@ -887,6 +897,10 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/matrix/ @PaarthShah
|
||||
/homeassistant/components/matter/ @home-assistant/matter
|
||||
/tests/components/matter/ @home-assistant/matter
|
||||
/homeassistant/components/mcp/ @allenporter
|
||||
/tests/components/mcp/ @allenporter
|
||||
/homeassistant/components/mcp_server/ @allenporter
|
||||
/tests/components/mcp_server/ @allenporter
|
||||
/homeassistant/components/mealie/ @joostlek @andrew-codechimp
|
||||
/tests/components/mealie/ @joostlek @andrew-codechimp
|
||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||
@ -955,8 +969,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/motionblinds_ble/ @LennP @jerrybboy
|
||||
/homeassistant/components/motioneye/ @dermotduffy
|
||||
/tests/components/motioneye/ @dermotduffy
|
||||
/homeassistant/components/motionmount/ @RJPoelstra
|
||||
/tests/components/motionmount/ @RJPoelstra
|
||||
/homeassistant/components/motionmount/ @laiho-vogels
|
||||
/tests/components/motionmount/ @laiho-vogels
|
||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
@ -1016,7 +1030,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/nina/ @DeerMaximum
|
||||
/tests/components/nina/ @DeerMaximum
|
||||
/homeassistant/components/nissan_leaf/ @filcole
|
||||
/homeassistant/components/nmbs/ @thibmaek
|
||||
/homeassistant/components/noaa_tides/ @jdelaney72
|
||||
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
||||
/tests/components/nobo_hub/ @echoromeo @oyvindwe
|
||||
@ -1040,8 +1053,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/numato/ @clssn
|
||||
/homeassistant/components/number/ @home-assistant/core @Shulyaka
|
||||
/tests/components/number/ @home-assistant/core @Shulyaka
|
||||
/homeassistant/components/nut/ @bdraco @ollo69 @pestevez
|
||||
/tests/components/nut/ @bdraco @ollo69 @pestevez
|
||||
/homeassistant/components/nut/ @bdraco @ollo69 @pestevez @tdfountain
|
||||
/tests/components/nut/ @bdraco @ollo69 @pestevez @tdfountain
|
||||
/homeassistant/components/nws/ @MatthewFlamm @kamiyo
|
||||
/tests/components/nws/ @MatthewFlamm @kamiyo
|
||||
/homeassistant/components/nyt_games/ @joostlek
|
||||
@ -1064,12 +1077,14 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/oncue/ @bdraco @peterager
|
||||
/homeassistant/components/ondilo_ico/ @JeromeHXP
|
||||
/tests/components/ondilo_ico/ @JeromeHXP
|
||||
/homeassistant/components/onedrive/ @zweckj
|
||||
/tests/components/onedrive/ @zweckj
|
||||
/homeassistant/components/onewire/ @garbled1 @epenet
|
||||
/tests/components/onewire/ @garbled1 @epenet
|
||||
/homeassistant/components/onkyo/ @arturpragacz
|
||||
/tests/components/onkyo/ @arturpragacz
|
||||
/homeassistant/components/onvif/ @hunterjm
|
||||
/tests/components/onvif/ @hunterjm
|
||||
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
||||
/tests/components/onkyo/ @arturpragacz @eclair4151
|
||||
/homeassistant/components/onvif/ @hunterjm @jterrace
|
||||
/tests/components/onvif/ @hunterjm @jterrace
|
||||
/homeassistant/components/open_meteo/ @frenck
|
||||
/tests/components/open_meteo/ @frenck
|
||||
/homeassistant/components/openai_conversation/ @balloob
|
||||
@ -1103,8 +1118,10 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/otbr/ @home-assistant/core
|
||||
/homeassistant/components/ourgroceries/ @OnFreund
|
||||
/tests/components/ourgroceries/ @OnFreund
|
||||
/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||
/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 @alexfp14
|
||||
/homeassistant/components/overkiz/ @imicknl
|
||||
/tests/components/overkiz/ @imicknl
|
||||
/homeassistant/components/overseerr/ @joostlek
|
||||
/tests/components/overseerr/ @joostlek
|
||||
/homeassistant/components/ovo_energy/ @timmo001
|
||||
/tests/components/ovo_energy/ @timmo001
|
||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||
@ -1113,6 +1130,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/palazzetti/ @dotvav
|
||||
/homeassistant/components/panel_custom/ @home-assistant/frontend
|
||||
/tests/components/panel_custom/ @home-assistant/frontend
|
||||
/homeassistant/components/peblar/ @frenck
|
||||
/tests/components/peblar/ @frenck
|
||||
/homeassistant/components/peco/ @IceBotYT
|
||||
/tests/components/peco/ @IceBotYT
|
||||
/homeassistant/components/pegel_online/ @mib1185
|
||||
@ -1121,20 +1140,22 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/permobil/ @IsakNyberg
|
||||
/homeassistant/components/persistent_notification/ @home-assistant/core
|
||||
/tests/components/persistent_notification/ @home-assistant/core
|
||||
/homeassistant/components/pglab/ @pglab-electronics
|
||||
/tests/components/pglab/ @pglab-electronics
|
||||
/homeassistant/components/philips_js/ @elupus
|
||||
/tests/components/philips_js/ @elupus
|
||||
/homeassistant/components/pi_hole/ @shenxn
|
||||
/tests/components/pi_hole/ @shenxn
|
||||
/homeassistant/components/picnic/ @corneyl
|
||||
/tests/components/picnic/ @corneyl
|
||||
/homeassistant/components/picnic/ @corneyl @codesalatdev
|
||||
/tests/components/picnic/ @corneyl @codesalatdev
|
||||
/homeassistant/components/ping/ @jpbede
|
||||
/tests/components/ping/ @jpbede
|
||||
/homeassistant/components/plaato/ @JohNan
|
||||
/tests/components/plaato/ @JohNan
|
||||
/homeassistant/components/plex/ @jjlawren
|
||||
/tests/components/plex/ @jjlawren
|
||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew @frenck
|
||||
/tests/components/plugwise/ @CoMPaTech @bouwew @frenck
|
||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||
/tests/components/plugwise/ @CoMPaTech @bouwew
|
||||
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||
/homeassistant/components/point/ @fredrike
|
||||
@ -1180,6 +1201,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/pyload/ @tr4nt0r
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
/homeassistant/components/qbus/ @Qbus-iot @thomasddn
|
||||
/tests/components/qbus/ @Qbus-iot @thomasddn
|
||||
/homeassistant/components/qingping/ @bdraco
|
||||
/tests/components/qingping/ @bdraco
|
||||
/homeassistant/components/qld_bushfire/ @exxamalte
|
||||
@ -1256,8 +1279,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||
/homeassistant/components/rmvtransport/ @cgtobi
|
||||
/tests/components/rmvtransport/ @cgtobi
|
||||
/homeassistant/components/roborock/ @Lash-L
|
||||
/tests/components/roborock/ @Lash-L
|
||||
/homeassistant/components/roborock/ @Lash-L @allenporter
|
||||
/tests/components/roborock/ @Lash-L @allenporter
|
||||
/homeassistant/components/roku/ @ctalkington
|
||||
/tests/components/roku/ @ctalkington
|
||||
/homeassistant/components/romy/ @xeniter
|
||||
@ -1276,6 +1299,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
|
||||
/homeassistant/components/russound_rio/ @noahhusby
|
||||
/tests/components/russound_rio/ @noahhusby
|
||||
/homeassistant/components/russound_rnet/ @noahhusby
|
||||
/homeassistant/components/ruuvi_gateway/ @akx
|
||||
/tests/components/ruuvi_gateway/ @akx
|
||||
/homeassistant/components/ruuvitag_ble/ @akx
|
||||
@ -1320,6 +1344,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sensorpro/ @bdraco
|
||||
/homeassistant/components/sensorpush/ @bdraco
|
||||
/tests/components/sensorpush/ @bdraco
|
||||
/homeassistant/components/sensorpush_cloud/ @sstallion
|
||||
/tests/components/sensorpush_cloud/ @sstallion
|
||||
/homeassistant/components/sensoterra/ @markruys
|
||||
/tests/components/sensoterra/ @markruys
|
||||
/homeassistant/components/sentry/ @dcramer @frenck
|
||||
@ -1369,12 +1395,14 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/slide_local/ @dontinelli
|
||||
/homeassistant/components/slimproto/ @marcelveldt
|
||||
/tests/components/slimproto/ @marcelveldt
|
||||
/homeassistant/components/sma/ @kellerza @rklomp
|
||||
/tests/components/sma/ @kellerza @rklomp
|
||||
/homeassistant/components/sma/ @kellerza @rklomp @erwindouna
|
||||
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
||||
/homeassistant/components/smappee/ @bsmappee
|
||||
/tests/components/smappee/ @bsmappee
|
||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||
/homeassistant/components/smartthings/ @joostlek
|
||||
/tests/components/smartthings/ @joostlek
|
||||
/homeassistant/components/smarttub/ @mdz
|
||||
/tests/components/smarttub/ @mdz
|
||||
/homeassistant/components/smarty/ @z0mbieprocess
|
||||
@ -1389,6 +1417,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/snapcast/ @luar123
|
||||
/homeassistant/components/snmp/ @nmaggioni
|
||||
/tests/components/snmp/ @nmaggioni
|
||||
/homeassistant/components/snoo/ @Lash-L
|
||||
/tests/components/snoo/ @Lash-L
|
||||
/homeassistant/components/snooz/ @AustinBrunkhorst
|
||||
/tests/components/snooz/ @AustinBrunkhorst
|
||||
/homeassistant/components/solaredge/ @frenck @bdraco
|
||||
@ -1396,8 +1426,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||
/homeassistant/components/solax/ @squishykid
|
||||
/tests/components/solax/ @squishykid
|
||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||
/tests/components/solax/ @squishykid @Darsstar
|
||||
/homeassistant/components/soma/ @ratsept @sebfortier2288
|
||||
/tests/components/soma/ @ratsept @sebfortier2288
|
||||
/homeassistant/components/sonarr/ @ctalkington
|
||||
@ -1476,8 +1506,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||
/tests/components/systemmonitor/ @gjohansson-ST
|
||||
/homeassistant/components/tado/ @chiefdragon @erwindouna
|
||||
/tests/components/tado/ @chiefdragon @erwindouna
|
||||
/homeassistant/components/tado/ @erwindouna
|
||||
/tests/components/tado/ @erwindouna
|
||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||
/tests/components/tag/ @balloob @dmulcahey
|
||||
/homeassistant/components/tailscale/ @frenck
|
||||
@ -1499,8 +1529,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tedee/ @patrickhilker @zweckj
|
||||
/homeassistant/components/tellduslive/ @fredrike
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/template/ @PhracturedBlue @home-assistant/core
|
||||
/tests/components/template/ @PhracturedBlue @home-assistant/core
|
||||
/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
/tests/components/tesla_fleet/ @Bre77
|
||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||
@ -1571,8 +1601,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/triggercmd/ @rvmey
|
||||
/homeassistant/components/tts/ @home-assistant/core
|
||||
/tests/components/tts/ @home-assistant/core
|
||||
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
|
||||
/tests/components/tuya/ @Tuya @zlinoliver @frenck
|
||||
/homeassistant/components/tuya/ @Tuya @zlinoliver
|
||||
/tests/components/tuya/ @Tuya @zlinoliver
|
||||
/homeassistant/components/twentemilieu/ @frenck
|
||||
/tests/components/twentemilieu/ @frenck
|
||||
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen
|
||||
@ -1616,15 +1646,15 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/valve/ @home-assistant/core
|
||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||
/tests/components/velbus/ @Cereal2nd @brefra
|
||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum
|
||||
/tests/components/velux/ @Julius2342 @DeerMaximum
|
||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
||||
/tests/components/venstar/ @garbled1 @jhollowe
|
||||
/homeassistant/components/versasense/ @imstevenxyz
|
||||
/homeassistant/components/version/ @ludeeus
|
||||
/tests/components/version/ @ludeeus
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
||||
/homeassistant/components/vicare/ @CFenner
|
||||
/tests/components/vicare/ @CFenner
|
||||
/homeassistant/components/vilfo/ @ManneW
|
||||
@ -1669,6 +1699,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/weatherflow_cloud/ @jeeftor
|
||||
/homeassistant/components/weatherkit/ @tjhorner
|
||||
/tests/components/weatherkit/ @tjhorner
|
||||
/homeassistant/components/webdav/ @jpbede
|
||||
/tests/components/webdav/ @jpbede
|
||||
/homeassistant/components/webhook/ @home-assistant/core
|
||||
/tests/components/webhook/ @home-assistant/core
|
||||
/homeassistant/components/webmin/ @autinerd
|
||||
@ -1740,6 +1772,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/youless/ @gjong
|
||||
/homeassistant/components/youtube/ @joostlek
|
||||
/tests/components/youtube/ @joostlek
|
||||
/homeassistant/components/zabbix/ @kruton
|
||||
/homeassistant/components/zamg/ @killer0071234
|
||||
/tests/components/zamg/ @killer0071234
|
||||
/homeassistant/components/zengge/ @emontnemery
|
||||
|
38
Dockerfile
generated
38
Dockerfile
generated
@ -12,8 +12,26 @@ ENV \
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.5.8
|
||||
RUN pip3 install uv==0.6.1
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
@ -42,22 +60,4 @@ RUN \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
WORKDIR /config
|
||||
|
10
build.yaml
10
build.yaml
@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.12.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.12.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.12.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.12.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.12.1
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@ -308,7 +308,7 @@ class AuthStore:
|
||||
credentials.data = data
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_load(self) -> None: # noqa: C901
|
||||
async def async_load(self) -> None:
|
||||
"""Load the users."""
|
||||
if self._loaded:
|
||||
raise RuntimeError("Auth storage is already loaded")
|
||||
|
@ -4,9 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import types
|
||||
from typing import Any, Generic
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import TypeVar
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
@ -35,12 +34,6 @@ DATA_REQS: HassKey[set[str]] = HassKey("mfa_auth_module_reqs_processed")
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MultiFactorAuthModuleT = TypeVar(
|
||||
"_MultiFactorAuthModuleT",
|
||||
bound="MultiFactorAuthModule",
|
||||
default="MultiFactorAuthModule",
|
||||
)
|
||||
|
||||
|
||||
class MultiFactorAuthModule:
|
||||
"""Multi-factor Auth Module of validation function."""
|
||||
@ -102,7 +95,9 @@ class MultiFactorAuthModule:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SetupFlow(data_entry_flow.FlowHandler, Generic[_MultiFactorAuthModuleT]):
|
||||
class SetupFlow[_MultiFactorAuthModuleT: MultiFactorAuthModule = MultiFactorAuthModule](
|
||||
data_entry_flow.FlowHandler
|
||||
):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(
|
||||
|
@ -11,7 +11,7 @@ import uuid
|
||||
import attr
|
||||
from attr import Attribute
|
||||
from attr.setters import validate
|
||||
from propcache import cached_property
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.const import __version__
|
||||
from homeassistant.data_entry_flow import FlowContext, FlowResult
|
||||
|
@ -17,12 +17,12 @@ POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
|
||||
|
||||
__all__ = [
|
||||
"POLICY_SCHEMA",
|
||||
"merge_policies",
|
||||
"PermissionLookup",
|
||||
"PolicyType",
|
||||
"AbstractPermissions",
|
||||
"PolicyPermissions",
|
||||
"OwnerPermissions",
|
||||
"PermissionLookup",
|
||||
"PolicyPermissions",
|
||||
"PolicyType",
|
||||
"merge_policies",
|
||||
]
|
||||
|
||||
|
||||
|
@ -5,9 +5,8 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
import types
|
||||
from typing import Any, Generic
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import TypeVar
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
@ -47,8 +46,6 @@ AUTH_PROVIDER_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_AuthProviderT = TypeVar("_AuthProviderT", bound="AuthProvider", default="AuthProvider")
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
@ -195,9 +192,8 @@ async def load_auth_provider_module(
|
||||
return module
|
||||
|
||||
|
||||
class LoginFlow(
|
||||
class LoginFlow[_AuthProviderT: AuthProvider = AuthProvider](
|
||||
FlowHandler[AuthFlowContext, AuthFlowResult, tuple[str, str]],
|
||||
Generic[_AuthProviderT],
|
||||
):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
|
@ -21,7 +21,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.network import is_cloud_connection
|
||||
|
||||
from .. import InvalidAuthError
|
||||
|
@ -18,6 +18,7 @@ import securetar
|
||||
from .const import __version__ as HA_VERSION
|
||||
|
||||
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
||||
RESTORE_BACKUP_RESULT_FILE = ".HA_RESTORE_RESULT"
|
||||
KEEP_BACKUPS = ("backups",)
|
||||
KEEP_DATABASE = (
|
||||
"home-assistant_v2.db",
|
||||
@ -62,8 +63,14 @@ def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent |
|
||||
restore_database=instruction_content["restore_database"],
|
||||
restore_homeassistant=instruction_content["restore_homeassistant"],
|
||||
)
|
||||
except (FileNotFoundError, KeyError, json.JSONDecodeError):
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except (KeyError, json.JSONDecodeError) as err:
|
||||
_write_restore_result_file(config_dir, False, err)
|
||||
return None
|
||||
finally:
|
||||
# Always remove the backup instruction file to prevent a boot loop
|
||||
instruction_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _clear_configuration_directory(config_dir: Path, keep: Iterable[str]) -> None:
|
||||
@ -116,7 +123,7 @@ def _extract_backup(
|
||||
Path(
|
||||
tempdir,
|
||||
"extracted",
|
||||
f"homeassistant.tar{'.gz' if backup_meta["compressed"] else ''}",
|
||||
f"homeassistant.tar{'.gz' if backup_meta['compressed'] else ''}",
|
||||
),
|
||||
gzip=backup_meta["compressed"],
|
||||
key=password_to_key(restore_content.password)
|
||||
@ -139,6 +146,7 @@ def _extract_backup(
|
||||
config_dir,
|
||||
dirs_exist_ok=True,
|
||||
ignore=shutil.ignore_patterns(*(keep)),
|
||||
ignore_dangling_symlinks=True,
|
||||
)
|
||||
elif restore_content.restore_database:
|
||||
for entry in KEEP_DATABASE:
|
||||
@ -156,6 +164,23 @@ def _extract_backup(
|
||||
)
|
||||
|
||||
|
||||
def _write_restore_result_file(
|
||||
config_dir: Path, success: bool, error: Exception | None
|
||||
) -> None:
|
||||
"""Write the restore result file."""
|
||||
result_path = config_dir.joinpath(RESTORE_BACKUP_RESULT_FILE)
|
||||
result_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"success": success,
|
||||
"error": str(error) if error else None,
|
||||
"error_type": str(type(error).__name__) if error else None,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def restore_backup(config_dir_path: str) -> bool:
|
||||
"""Restore the backup file if any.
|
||||
|
||||
@ -174,7 +199,14 @@ def restore_backup(config_dir_path: str) -> bool:
|
||||
restore_content=restore_content,
|
||||
)
|
||||
except FileNotFoundError as err:
|
||||
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
||||
file_not_found = ValueError(f"Backup file {backup_file_path} does not exist")
|
||||
_write_restore_result_file(config_dir, False, file_not_found)
|
||||
raise file_not_found from err
|
||||
except Exception as err:
|
||||
_write_restore_result_file(config_dir, False, err)
|
||||
raise
|
||||
else:
|
||||
_write_restore_result_file(config_dir, True, None)
|
||||
if restore_content.remove_after_restore:
|
||||
backup_file_path.unlink(missing_ok=True)
|
||||
_LOGGER.info("Restore complete, restarting")
|
||||
|
@ -31,7 +31,7 @@ def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||
def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||
# If the file is in /proc we can ignore it.
|
||||
args = mapped_args["args"]
|
||||
path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
|
||||
path = args[0] if type(args[0]) is str else str(args[0])
|
||||
return path.startswith(ALLOWED_FILE_PREFIXES)
|
||||
|
||||
|
||||
|
@ -74,12 +74,14 @@ from .core_config import async_process_ha_core_config
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
backup,
|
||||
category_registry,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity,
|
||||
entity_registry,
|
||||
floor_registry,
|
||||
frame,
|
||||
issue_registry,
|
||||
label_registry,
|
||||
recorder,
|
||||
@ -89,8 +91,9 @@ from .helpers import (
|
||||
)
|
||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||
from .helpers.storage import get_internal_store_manager
|
||||
from .helpers.system_info import async_get_system_info, is_official_image
|
||||
from .helpers.system_info import async_get_system_info
|
||||
from .helpers.typing import ConfigType
|
||||
from .loader import Integration
|
||||
from .setup import (
|
||||
# _setup_started is marked as protected to make it clear
|
||||
# that it is not part of the public API and should not be used
|
||||
@ -106,11 +109,17 @@ from .util.async_ import create_eager_task
|
||||
from .util.hass_dict import HassKey
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
|
||||
from .util.system_info import is_official_image
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||
from anyio._backends import _asyncio # noqa: F401
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
# httpx will import trio if it is installed which does
|
||||
# blocking I/O in the event loop. We want to avoid that.
|
||||
import trio # noqa: F401
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .runner import RuntimeConfig
|
||||
@ -128,14 +137,12 @@ DATA_REGISTRIES_LOADED: HassKey[None] = HassKey("bootstrap_registries_loaded")
|
||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||
SLOW_STARTUP_CHECK_INTERVAL = 1
|
||||
|
||||
STAGE_0_SUBSTAGE_TIMEOUT = 60
|
||||
STAGE_1_TIMEOUT = 120
|
||||
STAGE_2_TIMEOUT = 300
|
||||
WRAP_UP_TIMEOUT = 300
|
||||
COOLDOWN_TIME = 60
|
||||
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {"debugpy"}
|
||||
|
||||
# Core integrations are unconditionally loaded
|
||||
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
|
||||
|
||||
@ -146,6 +153,10 @@ LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
|
||||
"isal",
|
||||
# Set log levels
|
||||
"logger",
|
||||
# Ensure network config is available
|
||||
# before hassio or any other integration is
|
||||
# loaded that might create an aiohttp client session
|
||||
"network",
|
||||
# Error logging
|
||||
"system_log",
|
||||
"sentry",
|
||||
@ -156,12 +167,27 @@ FRONTEND_INTEGRATIONS = {
|
||||
# visible in frontend
|
||||
"frontend",
|
||||
}
|
||||
RECORDER_INTEGRATIONS = {
|
||||
# Setup after frontend
|
||||
# To record data
|
||||
"recorder",
|
||||
}
|
||||
DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb", "zeroconf")
|
||||
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
|
||||
# The substage containing recorder should have no timeout, as it could cancel a database migration.
|
||||
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
|
||||
# The substages preceding it should also have no timeout, until we ensure that the recorder
|
||||
# is not accidentally promoted as a dependency of any of the integrations in them.
|
||||
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
|
||||
STAGE_0_INTEGRATIONS = (
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
|
||||
# Setup frontend
|
||||
("frontend", FRONTEND_INTEGRATIONS, None),
|
||||
# Setup recorder
|
||||
("recorder", {"recorder"}, None),
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
("debugger", {"debugpy"}, STAGE_0_SUBSTAGE_TIMEOUT),
|
||||
# Zeroconf is used for mdns resolution in aiohttp client helper.
|
||||
("zeroconf", {"zeroconf"}, STAGE_0_SUBSTAGE_TIMEOUT),
|
||||
)
|
||||
|
||||
DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb")
|
||||
# Stage 1 integrations are not to be preimported in bootstrap.
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
# We need to make sure discovery integrations
|
||||
# update their deps before stage 2 integrations
|
||||
@ -176,6 +202,7 @@ STAGE_1_INTEGRATIONS = {
|
||||
# Ensure supervisor is available
|
||||
"hassio",
|
||||
}
|
||||
|
||||
DEFAULT_INTEGRATIONS = {
|
||||
# These integrations are set up unless recovery mode is activated.
|
||||
#
|
||||
@ -216,22 +243,12 @@ DEFAULT_INTEGRATIONS_SUPERVISOR = {
|
||||
# These integrations are set up if using the Supervisor
|
||||
"hassio",
|
||||
}
|
||||
|
||||
CRITICAL_INTEGRATIONS = {
|
||||
# Recovery mode is activated if these integrations fail to set up
|
||||
"frontend",
|
||||
}
|
||||
|
||||
SETUP_ORDER = (
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
||||
# Setup frontend
|
||||
("frontend", FRONTEND_INTEGRATIONS),
|
||||
# Setup recorder
|
||||
("recorder", RECORDER_INTEGRATIONS),
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
("debugger", DEBUGGER_INTEGRATIONS),
|
||||
)
|
||||
|
||||
#
|
||||
# Storage keys we are likely to load during startup
|
||||
# in order of when we expect to load them.
|
||||
@ -252,6 +269,7 @@ PRELOAD_STORAGE = [
|
||||
"assist_pipeline.pipelines",
|
||||
"core.analytics",
|
||||
"auth_module.totp",
|
||||
"backup",
|
||||
]
|
||||
|
||||
|
||||
@ -282,14 +300,6 @@ async def async_setup_hass(
|
||||
|
||||
return hass
|
||||
|
||||
async def stop_hass(hass: core.HomeAssistant) -> None:
|
||||
"""Stop hass."""
|
||||
# Ask integrations to shut down. It's messy but we can't
|
||||
# do a clean stop without knowing what is broken
|
||||
with contextlib.suppress(TimeoutError):
|
||||
async with hass.timeout.async_timeout(10):
|
||||
await hass.async_stop()
|
||||
|
||||
hass = await create_hass()
|
||||
|
||||
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
|
||||
@ -305,10 +315,10 @@ async def async_setup_hass(
|
||||
|
||||
block_async_io.enable()
|
||||
|
||||
config_dict = None
|
||||
basic_setup_success = False
|
||||
|
||||
if not (recovery_mode := runtime_config.recovery_mode):
|
||||
config_dict = None
|
||||
basic_setup_success = False
|
||||
|
||||
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
try:
|
||||
@ -326,39 +336,43 @@ async def async_setup_hass(
|
||||
await async_from_config_dict(config_dict, hass) is not None
|
||||
)
|
||||
|
||||
if config_dict is None:
|
||||
recovery_mode = True
|
||||
await stop_hass(hass)
|
||||
hass = await create_hass()
|
||||
if config_dict is None:
|
||||
recovery_mode = True
|
||||
await hass.async_stop(force=True)
|
||||
hass = await create_hass()
|
||||
|
||||
elif not basic_setup_success:
|
||||
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
|
||||
recovery_mode = True
|
||||
await stop_hass(hass)
|
||||
hass = await create_hass()
|
||||
elif not basic_setup_success:
|
||||
_LOGGER.warning(
|
||||
"Unable to set up core integrations. Activating recovery mode"
|
||||
)
|
||||
recovery_mode = True
|
||||
await hass.async_stop(force=True)
|
||||
hass = await create_hass()
|
||||
|
||||
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
|
||||
_LOGGER.warning(
|
||||
"Detected that %s did not load. Activating recovery mode",
|
||||
",".join(CRITICAL_INTEGRATIONS),
|
||||
)
|
||||
elif any(
|
||||
domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Detected that %s did not load. Activating recovery mode",
|
||||
",".join(CRITICAL_INTEGRATIONS),
|
||||
)
|
||||
|
||||
old_config = hass.config
|
||||
old_logging = hass.data.get(DATA_LOGGING)
|
||||
old_config = hass.config
|
||||
old_logging = hass.data.get(DATA_LOGGING)
|
||||
|
||||
recovery_mode = True
|
||||
await stop_hass(hass)
|
||||
hass = await create_hass()
|
||||
recovery_mode = True
|
||||
await hass.async_stop(force=True)
|
||||
hass = await create_hass()
|
||||
|
||||
if old_logging:
|
||||
hass.data[DATA_LOGGING] = old_logging
|
||||
hass.config.debug = old_config.debug
|
||||
hass.config.skip_pip = old_config.skip_pip
|
||||
hass.config.skip_pip_packages = old_config.skip_pip_packages
|
||||
hass.config.internal_url = old_config.internal_url
|
||||
hass.config.external_url = old_config.external_url
|
||||
# Setup loader cache after the config dir has been set
|
||||
loader.async_setup(hass)
|
||||
if old_logging:
|
||||
hass.data[DATA_LOGGING] = old_logging
|
||||
hass.config.debug = old_config.debug
|
||||
hass.config.skip_pip = old_config.skip_pip
|
||||
hass.config.skip_pip_packages = old_config.skip_pip_packages
|
||||
hass.config.internal_url = old_config.internal_url
|
||||
hass.config.external_url = old_config.external_url
|
||||
# Setup loader cache after the config dir has been set
|
||||
loader.async_setup(hass)
|
||||
|
||||
if recovery_mode:
|
||||
_LOGGER.info("Starting in recovery mode")
|
||||
@ -421,9 +435,10 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
if DATA_REGISTRIES_LOADED in hass.data:
|
||||
return
|
||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||
translation.async_setup(hass)
|
||||
entity.async_setup(hass)
|
||||
frame.async_setup(hass)
|
||||
template.async_setup(hass)
|
||||
translation.async_setup(hass)
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass)),
|
||||
@ -644,11 +659,10 @@ def _create_log_file(
|
||||
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
|
||||
err_log_path, backupCount=1
|
||||
)
|
||||
|
||||
try:
|
||||
err_handler.doRollover()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error rolling over log file: %s", err)
|
||||
try:
|
||||
err_handler.doRollover()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error rolling over log file: %s", err)
|
||||
|
||||
return err_handler
|
||||
|
||||
@ -677,7 +691,6 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
return deps_dir
|
||||
|
||||
|
||||
@core.callback
|
||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||
"""Get domains of components to set up."""
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
@ -699,6 +712,258 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||
return domains
|
||||
|
||||
|
||||
async def _async_resolve_domains_and_preload(
|
||||
hass: core.HomeAssistant, config: dict[str, Any]
|
||||
) -> tuple[dict[str, Integration], dict[str, Integration]]:
|
||||
"""Resolve all dependencies and return integrations to set up.
|
||||
|
||||
The return value is a tuple of two dictionaries:
|
||||
- The first dictionary contains integrations
|
||||
specified by the configuration (including config entries).
|
||||
- The second dictionary contains the same integrations as the first dictionary
|
||||
together with all their dependencies.
|
||||
"""
|
||||
domains_to_setup = _get_domains(hass, config)
|
||||
platform_integrations = conf_util.extract_platform_integrations(
|
||||
config, BASE_PLATFORMS
|
||||
)
|
||||
# Ensure base platforms that have platform integrations are added to `domains`,
|
||||
# so they can be setup first instead of discovering them later when a config
|
||||
# entry setup task notices that it's needed and there is already a long line
|
||||
# to use the import executor.
|
||||
#
|
||||
# For example if we have
|
||||
# sensor:
|
||||
# - platform: template
|
||||
#
|
||||
# `template` has to be loaded to validate the config for sensor
|
||||
# so we want to start loading `sensor` as soon as we know
|
||||
# it will be needed. The more platforms under `sensor:`, the longer
|
||||
# it will take to finish setup for `sensor` because each of these
|
||||
# platforms has to be imported before we can validate the config.
|
||||
#
|
||||
# Thankfully we are migrating away from the platform pattern
|
||||
# so this will be less of a problem in the future.
|
||||
domains_to_setup.update(platform_integrations)
|
||||
|
||||
# Additionally process base platforms since we do not require the manifest
|
||||
# to list them as dependencies.
|
||||
# We want to later avoid lock contention when multiple integrations try to load
|
||||
# their manifests at once.
|
||||
# Also process integrations that are defined under base platforms
|
||||
# to speed things up.
|
||||
additional_domains_to_process = {
|
||||
*BASE_PLATFORMS,
|
||||
*chain.from_iterable(platform_integrations.values()),
|
||||
}
|
||||
|
||||
# Resolve all dependencies so we know all integrations
|
||||
# that will have to be loaded and start right-away
|
||||
integrations_or_excs = await loader.async_get_integrations(
|
||||
hass, {*domains_to_setup, *additional_domains_to_process}
|
||||
)
|
||||
# Eliminate those missing or with invalid manifest
|
||||
integrations_to_process = {
|
||||
domain: itg
|
||||
for domain, itg in integrations_or_excs.items()
|
||||
if isinstance(itg, Integration)
|
||||
}
|
||||
integrations_dependencies = await loader.resolve_integrations_dependencies(
|
||||
hass, integrations_to_process.values()
|
||||
)
|
||||
# Eliminate those without valid dependencies
|
||||
integrations_to_process = {
|
||||
domain: integrations_to_process[domain] for domain in integrations_dependencies
|
||||
}
|
||||
|
||||
integrations_to_setup = {
|
||||
domain: itg
|
||||
for domain, itg in integrations_to_process.items()
|
||||
if domain in domains_to_setup
|
||||
}
|
||||
all_integrations_to_setup = integrations_to_setup.copy()
|
||||
all_integrations_to_setup.update(
|
||||
(dep, loader.async_get_loaded_integration(hass, dep))
|
||||
for domain in integrations_to_setup
|
||||
for dep in integrations_dependencies[domain].difference(
|
||||
all_integrations_to_setup
|
||||
)
|
||||
)
|
||||
|
||||
# Gather requirements for all integrations,
|
||||
# their dependencies and after dependencies.
|
||||
# To gather all the requirements we must ignore exceptions here.
|
||||
# The exceptions will be detected and handled later in the bootstrap process.
|
||||
integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, integrations_to_process.values(), ignore_exceptions=True
|
||||
)
|
||||
)
|
||||
integrations_requirements = {
|
||||
domain: itg.requirements for domain, itg in integrations_to_process.items()
|
||||
}
|
||||
integrations_requirements.update(
|
||||
(dep, loader.async_get_loaded_integration(hass, dep).requirements)
|
||||
for deps in integrations_after_dependencies.values()
|
||||
for dep in deps.difference(integrations_requirements)
|
||||
)
|
||||
all_requirements = set(chain.from_iterable(integrations_requirements.values()))
|
||||
|
||||
# Optimistically check if requirements are already installed
|
||||
# ahead of setting up the integrations so we can prime the cache
|
||||
# We do not wait for this since it's an optimization only
|
||||
hass.async_create_background_task(
|
||||
requirements.async_load_installed_versions(hass, all_requirements),
|
||||
"check installed requirements",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
# Start loading translations for all integrations we are going to set up
|
||||
# in the background so they are ready when we need them. This avoids a
|
||||
# lot of waiting for the translation load lock and a thundering herd of
|
||||
# tasks trying to load the same translations at the same time as each
|
||||
# integration is loaded.
|
||||
#
|
||||
# We do not wait for this since as soon as the task runs it will
|
||||
# hold the translation load lock and if anything is fast enough to
|
||||
# wait for the translation load lock, loading will be done by the
|
||||
# time it gets to it.
|
||||
translations_to_load = {*all_integrations_to_setup, *additional_domains_to_process}
|
||||
hass.async_create_background_task(
|
||||
translation.async_load_integrations(hass, translations_to_load),
|
||||
"load translations",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
# Preload storage for all integrations we are going to set up
|
||||
# so we do not have to wait for it to be loaded when we need it
|
||||
# in the setup process.
|
||||
hass.async_create_background_task(
|
||||
get_internal_store_manager(hass).async_preload(
|
||||
[*PRELOAD_STORAGE, *all_integrations_to_setup]
|
||||
),
|
||||
"preload storage",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
return integrations_to_setup, all_integrations_to_setup
|
||||
|
||||
|
||||
async def _async_set_up_integrations(
|
||||
hass: core.HomeAssistant, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Set up all the integrations."""
|
||||
watcher = _WatchPendingSetups(hass, _setup_started(hass))
|
||||
watcher.async_start()
|
||||
|
||||
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
||||
hass, config
|
||||
)
|
||||
all_domains = set(all_integrations)
|
||||
domains = set(integrations)
|
||||
|
||||
_LOGGER.info(
|
||||
"Domains to be set up: %s | %s",
|
||||
domains,
|
||||
all_domains - domains,
|
||||
)
|
||||
|
||||
# Initialize recorder
|
||||
if "recorder" in all_domains:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
|
||||
# Initialize backup
|
||||
if "backup" in all_domains:
|
||||
backup.async_initialize_backup(hass)
|
||||
|
||||
stages: list[tuple[str, set[str], int | None]] = [
|
||||
*(
|
||||
(name, domain_group, timeout)
|
||||
for name, domain_group, timeout in STAGE_0_INTEGRATIONS
|
||||
),
|
||||
("1", STAGE_1_INTEGRATIONS, STAGE_1_TIMEOUT),
|
||||
("2", domains, STAGE_2_TIMEOUT),
|
||||
]
|
||||
|
||||
_LOGGER.info("Setting up stage 0")
|
||||
for name, domain_group, timeout in stages:
|
||||
stage_domains_unfiltered = domain_group & all_domains
|
||||
if not stage_domains_unfiltered:
|
||||
_LOGGER.info("Nothing to set up in stage %s: %s", name, domain_group)
|
||||
continue
|
||||
|
||||
stage_domains = stage_domains_unfiltered - hass.config.components
|
||||
if not stage_domains:
|
||||
_LOGGER.info("Already set up stage %s: %s", name, stage_domains_unfiltered)
|
||||
continue
|
||||
|
||||
stage_dep_domains_unfiltered = {
|
||||
dep
|
||||
for domain in stage_domains
|
||||
for dep in all_integrations[domain].all_dependencies
|
||||
if dep not in stage_domains
|
||||
}
|
||||
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
|
||||
|
||||
stage_all_domains = stage_domains | stage_dep_domains
|
||||
stage_all_integrations = {
|
||||
domain: all_integrations[domain] for domain in stage_all_domains
|
||||
}
|
||||
# Detect all cycles
|
||||
stage_integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, stage_all_integrations.values(), stage_all_domains
|
||||
)
|
||||
)
|
||||
stage_all_domains = set(stage_integrations_after_dependencies)
|
||||
stage_domains &= stage_all_domains
|
||||
stage_dep_domains &= stage_all_domains
|
||||
|
||||
_LOGGER.info(
|
||||
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
||||
name,
|
||||
stage_domains,
|
||||
stage_domains_unfiltered - stage_domains,
|
||||
stage_dep_domains,
|
||||
stage_dep_domains_unfiltered - stage_dep_domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, stage_all_domains)
|
||||
|
||||
if timeout is None:
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
try:
|
||||
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for stage %s waiting on %s - moving forward",
|
||||
name,
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
try:
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
await hass.async_block_till_done()
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for bootstrap waiting on %s - moving forward",
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
watcher.async_stop()
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
setup_time = async_get_setup_timings(hass)
|
||||
_LOGGER.debug(
|
||||
"Integration setup times: %s",
|
||||
dict(sorted(setup_time.items(), key=itemgetter(1), reverse=True)),
|
||||
)
|
||||
|
||||
|
||||
class _WatchPendingSetups:
|
||||
"""Periodic log and dispatch of setups that are pending."""
|
||||
|
||||
@ -770,14 +1035,12 @@ class _WatchPendingSetups:
|
||||
self._handle = None
|
||||
|
||||
|
||||
async def async_setup_multi_components(
|
||||
async def _async_setup_multi_components(
|
||||
hass: core.HomeAssistant,
|
||||
domains: set[str],
|
||||
config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Set up multiple domains. Log on failure."""
|
||||
# Avoid creating tasks for domains that were setup in a previous stage
|
||||
domains_not_yet_setup = domains - hass.config.components
|
||||
# Create setup tasks for base platforms first since everything will have
|
||||
# to wait to be imported, and the sooner we can get the base platforms
|
||||
# loaded the sooner we can start loading the rest of the integrations.
|
||||
@ -787,9 +1050,7 @@ async def async_setup_multi_components(
|
||||
f"setup component {domain}",
|
||||
eager_start=True,
|
||||
)
|
||||
for domain in sorted(
|
||||
domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True
|
||||
)
|
||||
for domain in sorted(domains, key=SETUP_ORDER_SORT_KEY, reverse=True)
|
||||
}
|
||||
results = await asyncio.gather(*futures.values(), return_exceptions=True)
|
||||
for idx, domain in enumerate(futures):
|
||||
@ -800,278 +1061,3 @@ async def async_setup_multi_components(
|
||||
domain,
|
||||
exc_info=(type(result), result, result.__traceback__),
|
||||
)
|
||||
|
||||
|
||||
async def _async_resolve_domains_to_setup(
|
||||
hass: core.HomeAssistant, config: dict[str, Any]
|
||||
) -> tuple[set[str], dict[str, loader.Integration]]:
|
||||
"""Resolve all dependencies and return list of domains to set up."""
|
||||
domains_to_setup = _get_domains(hass, config)
|
||||
needed_requirements: set[str] = set()
|
||||
platform_integrations = conf_util.extract_platform_integrations(
|
||||
config, BASE_PLATFORMS
|
||||
)
|
||||
# Ensure base platforms that have platform integrations are added to
|
||||
# to `domains_to_setup so they can be setup first instead of
|
||||
# discovering them when later when a config entry setup task
|
||||
# notices its needed and there is already a long line to use
|
||||
# the import executor.
|
||||
#
|
||||
# For example if we have
|
||||
# sensor:
|
||||
# - platform: template
|
||||
#
|
||||
# `template` has to be loaded to validate the config for sensor
|
||||
# so we want to start loading `sensor` as soon as we know
|
||||
# it will be needed. The more platforms under `sensor:`, the longer
|
||||
# it will take to finish setup for `sensor` because each of these
|
||||
# platforms has to be imported before we can validate the config.
|
||||
#
|
||||
# Thankfully we are migrating away from the platform pattern
|
||||
# so this will be less of a problem in the future.
|
||||
domains_to_setup.update(platform_integrations)
|
||||
|
||||
# Load manifests for base platforms and platform based integrations
|
||||
# that are defined under base platforms right away since we do not require
|
||||
# the manifest to list them as dependencies and we want to avoid the lock
|
||||
# contention when multiple integrations try to load them at once
|
||||
additional_manifests_to_load = {
|
||||
*BASE_PLATFORMS,
|
||||
*chain.from_iterable(platform_integrations.values()),
|
||||
}
|
||||
|
||||
translations_to_load = additional_manifests_to_load.copy()
|
||||
|
||||
# Resolve all dependencies so we know all integrations
|
||||
# that will have to be loaded and start right-away
|
||||
integration_cache: dict[str, loader.Integration] = {}
|
||||
to_resolve: set[str] = domains_to_setup
|
||||
while to_resolve or additional_manifests_to_load:
|
||||
old_to_resolve: set[str] = to_resolve
|
||||
to_resolve = set()
|
||||
|
||||
if additional_manifests_to_load:
|
||||
to_get = {*old_to_resolve, *additional_manifests_to_load}
|
||||
additional_manifests_to_load.clear()
|
||||
else:
|
||||
to_get = old_to_resolve
|
||||
|
||||
manifest_deps: set[str] = set()
|
||||
resolve_dependencies_tasks: list[asyncio.Task[bool]] = []
|
||||
integrations_to_process: list[loader.Integration] = []
|
||||
|
||||
for domain, itg in (await loader.async_get_integrations(hass, to_get)).items():
|
||||
if not isinstance(itg, loader.Integration):
|
||||
continue
|
||||
integration_cache[domain] = itg
|
||||
needed_requirements.update(itg.requirements)
|
||||
|
||||
# Make sure manifests for dependencies are loaded in the next
|
||||
# loop to try to group as many as manifest loads in a single
|
||||
# call to avoid the creating one-off executor jobs later in
|
||||
# the setup process
|
||||
additional_manifests_to_load.update(
|
||||
dep
|
||||
for dep in chain(itg.dependencies, itg.after_dependencies)
|
||||
if dep not in integration_cache
|
||||
)
|
||||
|
||||
if domain not in old_to_resolve:
|
||||
continue
|
||||
|
||||
integrations_to_process.append(itg)
|
||||
manifest_deps.update(itg.dependencies)
|
||||
manifest_deps.update(itg.after_dependencies)
|
||||
if not itg.all_dependencies_resolved:
|
||||
resolve_dependencies_tasks.append(
|
||||
create_eager_task(
|
||||
itg.resolve_dependencies(),
|
||||
name=f"resolve dependencies {domain}",
|
||||
loop=hass.loop,
|
||||
)
|
||||
)
|
||||
|
||||
if unseen_deps := manifest_deps - integration_cache.keys():
|
||||
# If there are dependencies, try to preload all
|
||||
# the integrations manifest at once and add them
|
||||
# to the list of requirements we need to install
|
||||
# so we can try to check if they are already installed
|
||||
# in a single call below which avoids each integration
|
||||
# having to wait for the lock to do it individually
|
||||
deps = await loader.async_get_integrations(hass, unseen_deps)
|
||||
for dependant_domain, dependant_itg in deps.items():
|
||||
if isinstance(dependant_itg, loader.Integration):
|
||||
integration_cache[dependant_domain] = dependant_itg
|
||||
needed_requirements.update(dependant_itg.requirements)
|
||||
|
||||
if resolve_dependencies_tasks:
|
||||
await asyncio.gather(*resolve_dependencies_tasks)
|
||||
|
||||
for itg in integrations_to_process:
|
||||
try:
|
||||
all_deps = itg.all_dependencies
|
||||
except RuntimeError:
|
||||
# Integration.all_dependencies raises RuntimeError if
|
||||
# dependencies could not be resolved
|
||||
continue
|
||||
for dep in all_deps:
|
||||
if dep in domains_to_setup:
|
||||
continue
|
||||
domains_to_setup.add(dep)
|
||||
to_resolve.add(dep)
|
||||
|
||||
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
|
||||
|
||||
# Optimistically check if requirements are already installed
|
||||
# ahead of setting up the integrations so we can prime the cache
|
||||
# We do not wait for this since its an optimization only
|
||||
hass.async_create_background_task(
|
||||
requirements.async_load_installed_versions(hass, needed_requirements),
|
||||
"check installed requirements",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
#
|
||||
# Only add the domains_to_setup after we finish resolving
|
||||
# as new domains are likely to added in the process
|
||||
#
|
||||
translations_to_load.update(domains_to_setup)
|
||||
# Start loading translations for all integrations we are going to set up
|
||||
# in the background so they are ready when we need them. This avoids a
|
||||
# lot of waiting for the translation load lock and a thundering herd of
|
||||
# tasks trying to load the same translations at the same time as each
|
||||
# integration is loaded.
|
||||
#
|
||||
# We do not wait for this since as soon as the task runs it will
|
||||
# hold the translation load lock and if anything is fast enough to
|
||||
# wait for the translation load lock, loading will be done by the
|
||||
# time it gets to it.
|
||||
hass.async_create_background_task(
|
||||
translation.async_load_integrations(hass, translations_to_load),
|
||||
"load translations",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
# Preload storage for all integrations we are going to set up
|
||||
# so we do not have to wait for it to be loaded when we need it
|
||||
# in the setup process.
|
||||
hass.async_create_background_task(
|
||||
get_internal_store_manager(hass).async_preload(
|
||||
[*PRELOAD_STORAGE, *domains_to_setup]
|
||||
),
|
||||
"preload storage",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
return domains_to_setup, integration_cache
|
||||
|
||||
|
||||
async def _async_set_up_integrations(
|
||||
hass: core.HomeAssistant, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Set up all the integrations."""
|
||||
watcher = _WatchPendingSetups(hass, _setup_started(hass))
|
||||
watcher.async_start()
|
||||
|
||||
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
|
||||
hass, config
|
||||
)
|
||||
|
||||
# Initialize recorder
|
||||
if "recorder" in domains_to_setup:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
|
||||
pre_stage_domains = [
|
||||
(name, domains_to_setup & domain_group) for name, domain_group in SETUP_ORDER
|
||||
]
|
||||
|
||||
# calculate what components to setup in what stage
|
||||
stage_1_domains: set[str] = set()
|
||||
|
||||
# Find all dependencies of any dependency of any stage 1 integration that
|
||||
# we plan on loading and promote them to stage 1. This is done only to not
|
||||
# get misleading log messages
|
||||
deps_promotion: set[str] = STAGE_1_INTEGRATIONS
|
||||
while deps_promotion:
|
||||
old_deps_promotion = deps_promotion
|
||||
deps_promotion = set()
|
||||
|
||||
for domain in old_deps_promotion:
|
||||
if domain not in domains_to_setup or domain in stage_1_domains:
|
||||
continue
|
||||
|
||||
stage_1_domains.add(domain)
|
||||
|
||||
if (dep_itg := integration_cache.get(domain)) is None:
|
||||
continue
|
||||
|
||||
deps_promotion.update(dep_itg.all_dependencies)
|
||||
|
||||
stage_2_domains = domains_to_setup - stage_1_domains
|
||||
|
||||
for name, domain_group in pre_stage_domains:
|
||||
if domain_group:
|
||||
stage_2_domains -= domain_group
|
||||
_LOGGER.info("Setting up %s: %s", name, domain_group)
|
||||
to_be_loaded = domain_group.copy()
|
||||
to_be_loaded.update(
|
||||
dep
|
||||
for domain in domain_group
|
||||
if (integration := integration_cache.get(domain)) is not None
|
||||
for dep in integration.all_dependencies
|
||||
)
|
||||
async_set_domains_to_be_loaded(hass, to_be_loaded)
|
||||
await async_setup_multi_components(hass, domain_group, config)
|
||||
|
||||
# Enables after dependencies when setting up stage 1 domains
|
||||
async_set_domains_to_be_loaded(hass, stage_1_domains)
|
||||
|
||||
# Start setup
|
||||
if stage_1_domains:
|
||||
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
||||
try:
|
||||
async with hass.timeout.async_timeout(
|
||||
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
|
||||
):
|
||||
await async_setup_multi_components(hass, stage_1_domains, config)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for stage 1 waiting on %s - moving forward",
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
# Add after dependencies when setting up stage 2 domains
|
||||
async_set_domains_to_be_loaded(hass, stage_2_domains)
|
||||
|
||||
if stage_2_domains:
|
||||
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
|
||||
try:
|
||||
async with hass.timeout.async_timeout(
|
||||
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
|
||||
):
|
||||
await async_setup_multi_components(hass, stage_2_domains, config)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for stage 2 waiting on %s - moving forward",
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
try:
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
await hass.async_block_till_done()
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for bootstrap waiting on %s - moving forward",
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
watcher.async_stop()
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
setup_time = async_get_setup_timings(hass)
|
||||
_LOGGER.debug(
|
||||
"Integration setup times: %s",
|
||||
dict(sorted(setup_time.items(), key=itemgetter(1), reverse=True)),
|
||||
)
|
||||
|
@ -5,6 +5,7 @@
|
||||
"google_assistant",
|
||||
"google_assistant_sdk",
|
||||
"google_cloud",
|
||||
"google_drive",
|
||||
"google_generative_ai_conversation",
|
||||
"google_mail",
|
||||
"google_maps",
|
||||
|
@ -2,14 +2,17 @@
|
||||
"domain": "microsoft",
|
||||
"name": "Microsoft",
|
||||
"integrations": [
|
||||
"azure_data_explorer",
|
||||
"azure_devops",
|
||||
"azure_event_hub",
|
||||
"azure_service_bus",
|
||||
"azure_storage",
|
||||
"microsoft_face_detect",
|
||||
"microsoft_face_identify",
|
||||
"microsoft_face",
|
||||
"microsoft",
|
||||
"msteams",
|
||||
"onedrive",
|
||||
"xbox"
|
||||
]
|
||||
}
|
||||
|
5
homeassistant/brands/sensorpush.json
Normal file
5
homeassistant/brands/sensorpush.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "sensorpush",
|
||||
"name": "SensorPush",
|
||||
"integrations": ["sensorpush", "sensorpush_cloud"]
|
||||
}
|
@ -11,7 +11,7 @@ from homeassistant.components.alarm_control_panel import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
@ -19,7 +19,9 @@ from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode alarm control panel device."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from . import AbodeSystem
|
||||
@ -21,7 +21,9 @@ from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode binary sensor devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import AbodeSystem
|
||||
@ -26,7 +26,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode camera devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -7,7 +7,7 @@ from jaraco.abode.devices.cover import Cover
|
||||
from homeassistant.components.cover import CoverEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
@ -15,7 +15,9 @@ from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode cover devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
@ -26,7 +26,9 @@ from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode light devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -7,7 +7,7 @@ from jaraco.abode.devices.lock import Lock
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
@ -15,7 +15,9 @@ from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode lock devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
@ -61,7 +61,9 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode sensor devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -34,17 +34,17 @@
|
||||
"services": {
|
||||
"capture_image": {
|
||||
"name": "Capture image",
|
||||
"description": "Request a new image capture from a camera device.",
|
||||
"description": "Requests a new image capture from a camera device.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Entity id of the camera to request an image."
|
||||
"description": "Entity ID of the camera to request an image from."
|
||||
}
|
||||
}
|
||||
},
|
||||
"change_setting": {
|
||||
"name": "Change setting",
|
||||
"description": "Change an Abode system setting.",
|
||||
"description": "Changes an Abode system setting.",
|
||||
"fields": {
|
||||
"setting": {
|
||||
"name": "Setting",
|
||||
@ -58,11 +58,11 @@
|
||||
},
|
||||
"trigger_automation": {
|
||||
"name": "Trigger automation",
|
||||
"description": "Trigger an Abode automation.",
|
||||
"description": "Triggers an Abode automation.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Entity id of the automation to trigger."
|
||||
"description": "Entity ID of the automation to trigger."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
@ -20,7 +20,9 @@ DEVICE_TYPES = ["switch", "valve"]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Abode switch devices."""
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AcaiaConfigEntry
|
||||
from .entity import AcaiaEntity
|
||||
@ -40,7 +40,7 @@ BINARY_SENSORS: tuple[AcaiaBinarySensorEntityDescription, ...] = (
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AcaiaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up binary sensors."""
|
||||
|
||||
|
@ -8,7 +8,7 @@ from aioacaia.acaiascale import AcaiaScale
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AcaiaConfigEntry
|
||||
from .entity import AcaiaEntity
|
||||
@ -45,7 +45,7 @@ BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = (
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AcaiaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up button entities and services."""
|
||||
|
||||
|
@ -25,5 +25,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioacaia"],
|
||||
"requirements": ["aioacaia==0.1.11"]
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioacaia==0.1.14"]
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ rules:
|
||||
No custom actions are defined.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: todo
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfMass, UnitOfVolumeFlowRate
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AcaiaConfigEntry
|
||||
from .entity import AcaiaEntity
|
||||
@ -77,7 +77,7 @@ RESTORE_SENSORS: tuple[AcaiaSensorEntityDescription, ...] = (
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AcaiaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors."""
|
||||
|
||||
|
@ -12,8 +12,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
@ -24,7 +24,7 @@ from homeassistant.components.weather import (
|
||||
|
||||
API_METRIC: Final = "Metric"
|
||||
ATTRIBUTION: Final = "Data provided by AccuWeather"
|
||||
ATTR_CATEGORY: Final = "Category"
|
||||
ATTR_CATEGORY_VALUE = "CategoryValue"
|
||||
ATTR_DIRECTION: Final = "Direction"
|
||||
ATTR_ENGLISH: Final = "English"
|
||||
ATTR_LEVEL: Final = "level"
|
||||
@ -55,5 +55,18 @@ CONDITION_MAP = {
|
||||
for cond_ha, cond_codes in CONDITION_CLASSES.items()
|
||||
for cond_code in cond_codes
|
||||
}
|
||||
AIR_QUALITY_CATEGORY_MAP = {
|
||||
1: "good",
|
||||
2: "moderate",
|
||||
3: "unhealthy",
|
||||
4: "very_unhealthy",
|
||||
5: "hazardous",
|
||||
}
|
||||
POLLEN_CATEGORY_MAP = {
|
||||
1: "low",
|
||||
2: "moderate",
|
||||
3: "high",
|
||||
4: "very_high",
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
|
@ -117,7 +117,9 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
|
||||
"""Update data via library."""
|
||||
try:
|
||||
async with timeout(10):
|
||||
result = await self.accuweather.async_get_daily_forecast()
|
||||
result = await self.accuweather.async_get_daily_forecast(
|
||||
language=self.hass.config.language
|
||||
)
|
||||
except EXCEPTIONS as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
|
@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["accuweather"],
|
||||
"requirements": ["accuweather==4.0.0"],
|
||||
"requirements": ["accuweather==4.2.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -25,12 +25,13 @@ from homeassistant.const import (
|
||||
UnitOfVolumetricFlux,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
AIR_QUALITY_CATEGORY_MAP,
|
||||
API_METRIC,
|
||||
ATTR_CATEGORY,
|
||||
ATTR_CATEGORY_VALUE,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_ENGLISH,
|
||||
ATTR_LEVEL,
|
||||
@ -38,6 +39,7 @@ from .const import (
|
||||
ATTR_VALUE,
|
||||
ATTRIBUTION,
|
||||
MAX_FORECAST_DAYS,
|
||||
POLLEN_CATEGORY_MAP,
|
||||
)
|
||||
from .coordinator import (
|
||||
AccuWeatherConfigEntry,
|
||||
@ -59,9 +61,9 @@ class AccuWeatherSensorDescription(SensorEntityDescription):
|
||||
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
AccuWeatherSensorDescription(
|
||||
key="AirQuality",
|
||||
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
|
||||
value_fn=lambda data: AIR_QUALITY_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
|
||||
options=list(AIR_QUALITY_CATEGORY_MAP.values()),
|
||||
translation_key="air_quality",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
@ -83,7 +85,9 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
attr_fn=lambda data: {
|
||||
ATTR_LEVEL: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
|
||||
},
|
||||
translation_key="grass_pollen",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
@ -107,7 +111,9 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
attr_fn=lambda data: {
|
||||
ATTR_LEVEL: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
|
||||
},
|
||||
translation_key="mold_pollen",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
@ -115,7 +121,9 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
attr_fn=lambda data: {
|
||||
ATTR_LEVEL: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
|
||||
},
|
||||
translation_key="ragweed_pollen",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
@ -181,14 +189,18 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
attr_fn=lambda data: {
|
||||
ATTR_LEVEL: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
|
||||
},
|
||||
translation_key="tree_pollen",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="UVIndex",
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
attr_fn=lambda data: {
|
||||
ATTR_LEVEL: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
|
||||
},
|
||||
translation_key="uv_index_forecast",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
@ -375,7 +387,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AccuWeatherConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add AccuWeather entities from a config_entry."""
|
||||
observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = (
|
||||
|
@ -16,7 +16,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
|
||||
"requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@ -26,10 +26,20 @@
|
||||
"state": {
|
||||
"good": "Good",
|
||||
"hazardous": "Hazardous",
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
"moderate": "Moderate",
|
||||
"unhealthy": "Unhealthy"
|
||||
"unhealthy": "Unhealthy",
|
||||
"very_unhealthy": "Very unhealthy"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]",
|
||||
"very_unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::very_unhealthy%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"apparent_temperature": {
|
||||
@ -62,12 +72,10 @@
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
"moderate": "Moderate",
|
||||
"very_high": "Very high"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,12 +89,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,6 +106,15 @@
|
||||
"steady": "Steady",
|
||||
"rising": "Rising",
|
||||
"falling": "Falling"
|
||||
},
|
||||
"state_attributes": {
|
||||
"options": {
|
||||
"state": {
|
||||
"falling": "[%key:component::accuweather::entity::sensor::pressure_tendency::state::falling%]",
|
||||
"rising": "[%key:component::accuweather::entity::sensor::pressure_tendency::state::rising%]",
|
||||
"steady": "[%key:component::accuweather::entity::sensor::pressure_tendency::state::steady%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ragweed_pollen": {
|
||||
@ -108,12 +123,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,12 +167,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,12 +181,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,12 +195,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from .const import (
|
||||
@ -54,7 +54,7 @@ PARALLEL_UPDATES = 1
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AccuWeatherConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add a AccuWeather weather entity from a config_entry."""
|
||||
async_add_entities([AccuWeatherEntity(entry.runtime_data)])
|
||||
|
@ -22,7 +22,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .hub import PulseHub
|
||||
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.components.cover import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AcmedaConfigEntry
|
||||
from .const import ACMEDA_HUB_UPDATE
|
||||
@ -22,7 +22,7 @@ from .helpers import async_add_acmeda_entities
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Acmeda Rollers from a config entry."""
|
||||
hub = config_entry.runtime_data
|
||||
|
@ -9,7 +9,7 @@ from aiopulse import Roller
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
@ -23,7 +23,7 @@ def async_add_acmeda_entities(
|
||||
entity_class: type,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
current: set[int],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add any new entities."""
|
||||
hub = config_entry.runtime_data
|
||||
|
@ -70,7 +70,7 @@ class PulseHub:
|
||||
|
||||
async def async_notify_update(self, update_type: aiopulse.UpdateType) -> None:
|
||||
"""Evaluate entities when hub reports that update has occurred."""
|
||||
LOGGER.debug("Hub {update_type.name} updated")
|
||||
LOGGER.debug("Hub %s updated", update_type.name)
|
||||
|
||||
if update_type == aiopulse.UpdateType.rollers:
|
||||
await update_devices(self.hass, self.config_entry, self.api.rollers)
|
||||
|
@ -6,7 +6,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AcmedaConfigEntry
|
||||
from .const import ACMEDA_HUB_UPDATE
|
||||
@ -17,7 +17,7 @@ from .helpers import async_add_acmeda_entities
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Acmeda Rollers from a config entry."""
|
||||
hub = config_entry.runtime_data
|
||||
|
@ -3,9 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import telnetlib # pylint: disable=deprecated-module
|
||||
from typing import Final
|
||||
|
||||
import telnetlib # pylint: disable=deprecated-module
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import LEASES_REGEX
|
||||
|
@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
|
||||
@ -33,7 +33,7 @@ from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax thermostat with config flow."""
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
|
@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
@ -34,9 +34,12 @@ from .const import (
|
||||
SERVICE_REMOVE_URL,
|
||||
)
|
||||
|
||||
SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url})
|
||||
SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): vol.Any(cv.url, cv.path)})
|
||||
SERVICE_ADD_URL_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url}
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_URL): vol.Any(cv.url, cv.path),
|
||||
}
|
||||
)
|
||||
SERVICE_REFRESH_SCHEMA = vol.Schema(
|
||||
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
||||
@ -120,12 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
|
||||
"""Unload AdGuard Home config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
loaded_entries = [
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if len(loaded_entries) == 1:
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
# This is the last loaded instance of AdGuard, deregister any services
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||
|
@ -12,7 +12,7 @@ from adguardhome import AdGuardHome
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN
|
||||
@ -85,7 +85,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdGuardConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdGuard Home sensor based on a config entry."""
|
||||
data = entry.runtime_data
|
||||
|
@ -79,7 +79,7 @@
|
||||
"services": {
|
||||
"add_url": {
|
||||
"name": "Add URL",
|
||||
"description": "Add a new filter subscription to AdGuard Home.",
|
||||
"description": "Adds a new filter subscription to AdGuard Home.",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
@ -123,11 +123,11 @@
|
||||
},
|
||||
"refresh": {
|
||||
"name": "Refresh",
|
||||
"description": "Refresh all filter subscriptions in AdGuard Home.",
|
||||
"description": "Refreshes all filter subscriptions in AdGuard Home.",
|
||||
"fields": {
|
||||
"force": {
|
||||
"name": "Force",
|
||||
"description": "Force update (bypasses AdGuard Home throttling). \"true\" to force, or \"false\" to omit for a regular refresh."
|
||||
"description": "Force update (bypasses AdGuard Home throttling), omit for a regular refresh."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ from adguardhome import AdGuardHome, AdGuardHomeError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN, LOGGER
|
||||
@ -79,7 +79,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdGuardConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdGuard Home switch based on a config entry."""
|
||||
data = entry.runtime_data
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_ADS_VAR, DATA_ADS, DOMAIN, AdsType
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.components.cover import (
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.components.select import (
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.switch import (
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.valve import (
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
|
@ -8,7 +8,7 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||
@ -20,7 +20,7 @@ PARALLEL_UPDATES = 0
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir Binary Sensor platform."""
|
||||
|
||||
@ -66,7 +66,7 @@ class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity):
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||
"""Initialize an Advantage Air Zone Motion sensor."""
|
||||
super().__init__(instance, ac_key, zone_key)
|
||||
self._attr_name = f'{self._zone["name"]} motion'
|
||||
self._attr_name = f"{self._zone['name']} motion"
|
||||
self._attr_unique_id += "-motion"
|
||||
|
||||
@property
|
||||
@ -84,7 +84,7 @@ class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity):
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||
"""Initialize an Advantage Air Zone MyZone sensor."""
|
||||
super().__init__(instance, ac_key, zone_key)
|
||||
self._attr_name = f'{self._zone["name"]} myZone'
|
||||
self._attr_name = f"{self._zone['name']} myZone"
|
||||
self._attr_unique_id += "-myzone"
|
||||
|
||||
@property
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@ -14,12 +15,13 @@ from homeassistant.components.climate import (
|
||||
FAN_MEDIUM,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import (
|
||||
@ -49,6 +51,14 @@ ADVANTAGE_AIR_MYTEMP_ENABLED = "climateControlModeEnabled"
|
||||
ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
|
||||
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
|
||||
ADVANTAGE_AIR_MYFAN = "autoAA"
|
||||
ADVANTAGE_AIR_MYAUTO_MODE_SET = "myAutoModeCurrentSetMode"
|
||||
|
||||
HVAC_ACTIONS = {
|
||||
"cool": HVACAction.COOLING,
|
||||
"heat": HVACAction.HEATING,
|
||||
"vent": HVACAction.FAN,
|
||||
"dry": HVACAction.DRYING,
|
||||
}
|
||||
|
||||
HVAC_MODES = [
|
||||
HVACMode.OFF,
|
||||
@ -76,7 +86,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir climate platform."""
|
||||
|
||||
@ -175,6 +185,17 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"])
|
||||
return HVACMode.OFF
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current running HVAC action."""
|
||||
if self._ac["state"] == ADVANTAGE_AIR_STATE_OFF:
|
||||
return HVACAction.OFF
|
||||
if self._ac["mode"] == "myauto":
|
||||
return HVAC_ACTIONS.get(
|
||||
self._ac.get(ADVANTAGE_AIR_MYAUTO_MODE_SET, HVACAction.OFF)
|
||||
)
|
||||
return HVAC_ACTIONS.get(self._ac["mode"])
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the current fan modes."""
|
||||
@ -273,6 +294,22 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
|
||||
return HVACMode.HEAT_COOL
|
||||
return HVACMode.OFF
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the HVAC action, inheriting from master AC if zone is open but idle if air is <= 5%."""
|
||||
if self._ac["state"] == ADVANTAGE_AIR_STATE_OFF:
|
||||
return HVACAction.OFF
|
||||
master_action = HVAC_ACTIONS.get(self._ac["mode"], HVACAction.OFF)
|
||||
if self._ac["mode"] == "myauto":
|
||||
master_action = HVAC_ACTIONS.get(
|
||||
str(self._ac.get(ADVANTAGE_AIR_MYAUTO_MODE_SET)), HVACAction.OFF
|
||||
)
|
||||
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
|
||||
if self._zone["value"] <= Decimal(5):
|
||||
return HVACAction.IDLE
|
||||
return master_action
|
||||
return HVACAction.OFF
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
|
@ -7,3 +7,4 @@ ADVANTAGE_AIR_STATE_CLOSE = "close"
|
||||
ADVANTAGE_AIR_STATE_ON = "on"
|
||||
ADVANTAGE_AIR_STATE_OFF = "off"
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED = "aaAutoFanModeEnabled"
|
||||
ADVANTAGE_AIR_NIGHT_MODE_ENABLED = "quietNightModeEnabled"
|
||||
|
@ -9,7 +9,7 @@ from homeassistant.components.cover import (
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN
|
||||
@ -22,7 +22,7 @@ PARALLEL_UPDATES = 0
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir cover platform."""
|
||||
|
||||
@ -41,7 +41,7 @@ async def async_setup_entry(
|
||||
entities.append(
|
||||
AdvantageAirThingCover(instance, thing, CoverDeviceClass.BLIND)
|
||||
)
|
||||
elif thing["channelDipState"] == 3: # 3 = "Garage door"
|
||||
elif thing["channelDipState"] in [3, 10]: # 3 & 10 = "Garage door"
|
||||
entities.append(
|
||||
AdvantageAirThingCover(instance, thing, CoverDeviceClass.GARAGE)
|
||||
)
|
||||
|
@ -5,7 +5,7 @@ from typing import Any
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
@ -16,7 +16,7 @@ from .models import AdvantageAirData
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir light platform."""
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .entity import AdvantageAirAcEntity
|
||||
@ -14,7 +14,7 @@ ADVANTAGE_AIR_INACTIVE = "Inactive"
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir select platform."""
|
||||
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_OPEN
|
||||
@ -32,7 +32,7 @@ PARALLEL_UPDATES = 0
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir sensor platform."""
|
||||
|
||||
@ -103,7 +103,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity):
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||
"""Initialize an Advantage Air Zone Vent Sensor."""
|
||||
super().__init__(instance, ac_key, zone_key=zone_key)
|
||||
self._attr_name = f'{self._zone["name"]} vent'
|
||||
self._attr_name = f"{self._zone['name']} vent"
|
||||
self._attr_unique_id += "-vent"
|
||||
|
||||
@property
|
||||
@ -131,7 +131,7 @@ class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity):
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||
"""Initialize an Advantage Air Zone wireless signal sensor."""
|
||||
super().__init__(instance, ac_key, zone_key)
|
||||
self._attr_name = f'{self._zone["name"]} signal'
|
||||
self._attr_name = f"{self._zone['name']} signal"
|
||||
self._attr_unique_id += "-signal"
|
||||
|
||||
@property
|
||||
@ -165,7 +165,7 @@ class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity):
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
|
||||
"""Initialize an Advantage Air Zone Temp Sensor."""
|
||||
super().__init__(instance, ac_key, zone_key)
|
||||
self._attr_name = f'{self._zone["name"]} temperature'
|
||||
self._attr_name = f"{self._zone['name']} temperature"
|
||||
self._attr_unique_id += "-temp"
|
||||
|
||||
@property
|
||||
|
@ -4,11 +4,12 @@ from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||
ADVANTAGE_AIR_NIGHT_MODE_ENABLED,
|
||||
ADVANTAGE_AIR_STATE_OFF,
|
||||
ADVANTAGE_AIR_STATE_ON,
|
||||
)
|
||||
@ -19,7 +20,7 @@ from .models import AdvantageAirData
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir switch platform."""
|
||||
|
||||
@ -32,6 +33,8 @@ async def async_setup_entry(
|
||||
entities.append(AdvantageAirFreshAir(instance, ac_key))
|
||||
if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]:
|
||||
entities.append(AdvantageAirMyFan(instance, ac_key))
|
||||
if ADVANTAGE_AIR_NIGHT_MODE_ENABLED in ac_device["info"]:
|
||||
entities.append(AdvantageAirNightMode(instance, ac_key))
|
||||
if things := instance.coordinator.data.get("myThings"):
|
||||
entities.extend(
|
||||
AdvantageAirRelay(instance, thing)
|
||||
@ -93,6 +96,32 @@ class AdvantageAirMyFan(AdvantageAirAcEntity, SwitchEntity):
|
||||
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: False})
|
||||
|
||||
|
||||
class AdvantageAirNightMode(AdvantageAirAcEntity, SwitchEntity):
|
||||
"""Representation of Advantage 'MySleep$aver' Mode control."""
|
||||
|
||||
_attr_icon = "mdi:weather-night"
|
||||
_attr_name = "MySleep$aver"
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
||||
"""Initialize an Advantage Air Night Mode control."""
|
||||
super().__init__(instance, ac_key)
|
||||
self._attr_unique_id += "-nightmode"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the Night Mode status."""
|
||||
return self._ac[ADVANTAGE_AIR_NIGHT_MODE_ENABLED]
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn Night Mode on."""
|
||||
await self.async_update_ac({ADVANTAGE_AIR_NIGHT_MODE_ENABLED: True})
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn Night Mode off."""
|
||||
await self.async_update_ac({ADVANTAGE_AIR_NIGHT_MODE_ENABLED: False})
|
||||
|
||||
|
||||
class AdvantageAirRelay(AdvantageAirThingEntity, SwitchEntity):
|
||||
"""Representation of Advantage Air Thing."""
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
from homeassistant.components.update import UpdateEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
@ -14,7 +14,7 @@ from .models import AdvantageAirData
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir update platform."""
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||
from aemet_opendata.interface import AEMET, ConnectionOptions, UpdateFeature
|
||||
@ -10,8 +11,9 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
|
||||
from .const import CONF_STATION_UPDATES, PLATFORMS
|
||||
from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DOMAIN, PLATFORMS
|
||||
from .coordinator import AemetConfigEntry, AemetData, WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -24,11 +26,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
update_features: int = UpdateFeature.FORECAST
|
||||
if entry.options.get(CONF_RADAR_UPDATES, False):
|
||||
update_features |= UpdateFeature.RADAR
|
||||
if entry.options.get(CONF_STATION_UPDATES, True):
|
||||
update_features |= UpdateFeature.STATION
|
||||
|
||||
options = ConnectionOptions(api_key, update_features)
|
||||
aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
|
||||
aemet.set_api_data_dir(hass.config.path(STORAGE_DIR, f"{DOMAIN}-{entry.unique_id}"))
|
||||
|
||||
try:
|
||||
await aemet.select_coordinates(latitude, longitude)
|
||||
except TownNotFound as err:
|
||||
@ -57,3 +63,11 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
await hass.async_add_executor_job(
|
||||
shutil.rmtree,
|
||||
hass.config.path(STORAGE_DIR, f"{DOMAIN}-{entry.unique_id}"),
|
||||
)
|
||||
|
@ -17,10 +17,11 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
|
||||
from .const import CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
||||
from .const import CONF_RADAR_UPDATES, CONF_STATION_UPDATES, DEFAULT_NAME, DOMAIN
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_RADAR_UPDATES, default=False): bool,
|
||||
vol.Required(CONF_STATION_UPDATES, default=True): bool,
|
||||
}
|
||||
)
|
||||
|
@ -51,8 +51,9 @@ from homeassistant.components.weather import (
|
||||
from homeassistant.const import Platform
|
||||
|
||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||
CONF_RADAR_UPDATES = "radar_updates"
|
||||
CONF_STATION_UPDATES = "station_updates"
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
PLATFORMS = [Platform.IMAGE, Platform.SENSOR, Platform.WEATHER]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aemet_opendata.const import AOD_COORDS
|
||||
from aemet_opendata.const import AOD_COORDS, AOD_IMG_BYTES
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import (
|
||||
@ -26,6 +26,7 @@ TO_REDACT_CONFIG = [
|
||||
|
||||
TO_REDACT_COORD = [
|
||||
AOD_COORDS,
|
||||
AOD_IMG_BYTES,
|
||||
]
|
||||
|
||||
|
||||
|
86
homeassistant/components/aemet/image.py
Normal file
86
homeassistant/components/aemet/image.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""Support for the AEMET OpenData images."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from aemet_opendata.const import AOD_DATETIME, AOD_IMG_BYTES, AOD_IMG_TYPE, AOD_RADAR
|
||||
from aemet_opendata.helpers import dict_nested_value
|
||||
|
||||
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
|
||||
AEMET_IMAGES: Final[tuple[ImageEntityDescription, ...]] = (
|
||||
ImageEntityDescription(
|
||||
key=AOD_RADAR,
|
||||
translation_key="weather_radar",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AemetConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData image entities based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
coordinator = domain_data.coordinator
|
||||
|
||||
unique_id = config_entry.unique_id
|
||||
assert unique_id is not None
|
||||
|
||||
async_add_entities(
|
||||
AemetImage(
|
||||
hass,
|
||||
name,
|
||||
coordinator,
|
||||
description,
|
||||
unique_id,
|
||||
)
|
||||
for description in AEMET_IMAGES
|
||||
if dict_nested_value(coordinator.data["lib"], [description.key]) is not None
|
||||
)
|
||||
|
||||
|
||||
class AemetImage(AemetEntity, ImageEntity):
|
||||
"""Implementation of an AEMET OpenData image."""
|
||||
|
||||
entity_description: ImageEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
description: ImageEntityDescription,
|
||||
unique_id: str,
|
||||
) -> None:
|
||||
"""Initialize the image."""
|
||||
super().__init__(coordinator, name, unique_id)
|
||||
ImageEntity.__init__(self, hass)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{unique_id}-{description.key}"
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update attributes when the coordinator updates."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update image attributes."""
|
||||
image_data = self.get_aemet_value([self.entity_description.key])
|
||||
self._cached_image = Image(
|
||||
content_type=image_data.get(AOD_IMG_TYPE),
|
||||
content=image_data.get(AOD_IMG_BYTES),
|
||||
)
|
||||
self._attr_image_last_updated = image_data.get(AOD_DATETIME)
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.6.3"]
|
||||
"requirements": ["AEMET-OpenData==0.6.4"]
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ from homeassistant.const import (
|
||||
UnitOfVolumetricFlux,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
@ -358,7 +358,7 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AemetConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
|
@ -18,10 +18,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"image": {
|
||||
"weather_radar": {
|
||||
"name": "Weather radar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"radar_updates": "Gather data from AEMET weather radar",
|
||||
"station_updates": "Gather data from AEMET weather stations"
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONDITIONS_MAP
|
||||
from .coordinator import AemetConfigEntry, WeatherUpdateCoordinator
|
||||
@ -35,7 +35,7 @@ from .entity import AemetEntity
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AemetConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData weather entity based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
|
@ -7,7 +7,7 @@ from typing import Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DOMAIN: Final = "aftership"
|
||||
|
||||
|
@ -9,12 +9,12 @@ from pyaftership import AfterShip, AfterShipException
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import AfterShipConfigEntry
|
||||
@ -42,7 +42,7 @@ PLATFORM_SCHEMA: Final = cv.removed(DOMAIN, raise_if_present=False)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AfterShipConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AfterShip sensor entities based on a config entry."""
|
||||
aftership = config_entry.runtime_data
|
||||
|
@ -51,7 +51,7 @@
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The {integration_title} YAML configuration import failed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ from homeassistant.components.alarm_control_panel import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import DOMAIN as AGENT_DOMAIN
|
||||
@ -24,7 +24,7 @@ CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel"
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AgentDVRConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Agent DVR Alarm Control Panels."""
|
||||
async_add_entities([AgentBaseStation(config_entry.runtime_data)])
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
|
||||
@ -39,7 +39,7 @@ CAMERA_SERVICES = {
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AgentDVRConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Agent cameras."""
|
||||
filter_urllib3_logging()
|
||||
|
@ -4,12 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from airgradient import AirGradientClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import AirGradientCoordinator
|
||||
from .coordinator import AirGradientConfigEntry, AirGradientCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
@ -21,9 +20,6 @@ PLATFORMS: list[Platform] = [
|
||||
]
|
||||
|
||||
|
||||
type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry) -> bool:
|
||||
"""Set up Airgradient from a config entry."""
|
||||
|
||||
@ -31,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirGradientConfigEntry)
|
||||
entry.data[CONF_HOST], session=async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
coordinator = AirGradientCoordinator(hass, client)
|
||||
coordinator = AirGradientCoordinator(hass, entry, client)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
@ -13,12 +13,14 @@ from homeassistant.components.button import (
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AirGradientConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirGradientCoordinator
|
||||
from .entity import AirGradientEntity
|
||||
from .entity import AirGradientEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@ -45,7 +47,7 @@ LED_BAR_TEST = AirGradientButtonEntityDescription(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AirGradientConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AirGradient button entities based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
@ -100,6 +102,7 @@ class AirGradientButton(AirGradientEntity, ButtonEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
|
||||
|
||||
@exception_handler
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self.entity_description.press_fn(self.coordinator.client)
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Config flow for Airgradient."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from airgradient import (
|
||||
@ -11,10 +12,15 @@ from airgradient import (
|
||||
from awesomeversion import AwesomeVersion
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@ -37,7 +43,7 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.client.set_configuration_control(ConfigurationControl.LOCAL)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
self.data[CONF_HOST] = host = discovery_info.host
|
||||
@ -95,10 +101,18 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(
|
||||
current_measures.serial_number, raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
if self.source == SOURCE_USER:
|
||||
self._abort_if_unique_id_configured()
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
await self.set_configuration_source()
|
||||
return self.async_create_entry(
|
||||
title=current_measures.model,
|
||||
if self.source == SOURCE_USER:
|
||||
return self.async_create_entry(
|
||||
title=current_measures.model,
|
||||
data={CONF_HOST: user_input[CONF_HOST]},
|
||||
)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data={CONF_HOST: user_input[CONF_HOST]},
|
||||
)
|
||||
return self.async_show_form(
|
||||
@ -106,3 +120,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration."""
|
||||
return await self.async_step_user()
|
||||
|
@ -4,18 +4,17 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AirGradientConfigEntry
|
||||
type AirGradientConfigEntry = ConfigEntry[AirGradientCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -32,11 +31,17 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
config_entry: AirGradientConfigEntry
|
||||
_current_version: str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: AirGradientConfigEntry,
|
||||
client: AirGradientClient,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"AirGradient {client.host}",
|
||||
update_interval=timedelta(minutes=1),
|
||||
)
|
||||
@ -55,7 +60,11 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
measures = await self.client.get_current_measures()
|
||||
config = await self.client.get_config()
|
||||
except AirGradientError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
if measures.firmware_version != self._current_version:
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_entry = device_registry.async_get_device(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user