Compare commits

..

2 Commits

Author SHA1 Message Date
epenet 478ec2b377 fix 2025-02-11 15:27:42 +00:00
epenet a66c3218f5 Do not test internals in flo tests 2025-02-11 15:15:51 +00:00
4180 changed files with 73602 additions and 293133 deletions
-1
View File
@@ -1,6 +1,5 @@
name: Report an issue with Home Assistant Core
description: Report an issue with Home Assistant Core.
type: Bug
body:
- type: markdown
attributes:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 65 KiB

-100
View File
@@ -1,100 +0,0 @@
# 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
+27 -27
View File
@@ -32,7 +32,7 @@ jobs:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.6.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.6.2
uses: actions/upload-artifact@v4.6.0
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@v9
uses: dawidd6/action-download-artifact@v8
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@v9
uses: dawidd6/action-download-artifact@v8
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.6.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.3.0
uses: actions/download-artifact@v4.1.8
with:
name: translations
@@ -190,14 +190,14 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2025.03.0
uses: home-assistant/builder@2024.08.2
with:
args: |
$BUILD_ARGS \
@@ -256,14 +256,14 @@ jobs:
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2025.03.0
uses: home-assistant/builder@2024.08.2
with:
args: |
$BUILD_ARGS \
@@ -324,20 +324,20 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Install Cosign
uses: sigstore/cosign-installer@v3.8.2
uses: sigstore/cosign-installer@v3.8.0
with:
cosign-release: "v2.2.3"
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.3.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -448,21 +448,18 @@ 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.6.0
uses: actions/setup-python@v5.4.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: translations
@@ -476,13 +473,16 @@ jobs:
run: |
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install build
pip install twine build
python -m build
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
skip-existing: true
- name: Upload package
shell: bash
run: |
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
twine upload dist/* --skip-existing
hassfest-image:
name: Build and test hassfest image
@@ -502,14 +502,14 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.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@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.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@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
+83 -152
View File
@@ -37,10 +37,10 @@ on:
type: boolean
env:
CACHE_VERSION: 12
CACHE_VERSION: 11
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.6"
HA_SHORT_VERSION: "2025.3"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
@@ -89,7 +89,6 @@ 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:
@@ -143,7 +142,6 @@ jobs:
test_group_count=10
tests="[]"
tests_glob=""
lint_only=""
skip_coverage=""
if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]];
@@ -194,17 +192,6 @@ 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
@@ -230,8 +217,6 @@ 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
@@ -249,13 +234,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache@v4.2.0
with:
path: venv
key: >-
@@ -271,7 +256,7 @@ jobs:
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.0
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
@@ -294,14 +279,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.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -310,7 +295,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.0
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -334,14 +319,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.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -350,7 +335,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.0
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -374,14 +359,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.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -390,7 +375,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.0
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -484,7 +469,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.6.0
uses: actions/setup-python@v5.4.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -497,7 +482,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.0
with:
path: venv
key: >-
@@ -505,7 +490,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.0
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -552,7 +537,7 @@ jobs:
python --version
uv pip freeze >> pip_freeze.txt
- name: Upload pip_freeze artifact
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: pip-freeze-${{ matrix.python-version }}
path: pip_freeze.txt
@@ -587,13 +572,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -620,13 +605,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -638,25 +623,6 @@ 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.6.0
with:
license-check: false # We use our own license audit checks
audit-licenses:
name: Audit licenses
runs-on: ubuntu-24.04
@@ -677,13 +643,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -695,7 +661,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.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
path: licenses-${{ matrix.python-version }}.json
@@ -720,13 +686,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -741,14 +707,14 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint homeassistant
pylint --ignore-missing-annotations=y homeassistant
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
python --version
pylint homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint-tests:
name: Check pylint on tests
@@ -767,13 +733,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -812,7 +778,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.0
uses: actions/setup-python@v5.4.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -825,7 +791,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -833,7 +799,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v4.2.3
uses: actions/cache@v4.2.0
with:
path: .mypy_cache
key: >-
@@ -863,7 +829,11 @@ jobs:
prepare-pytest-full:
runs-on: ubuntu-24.04
if: |
needs.info.outputs.lint_only != 'true'
(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.test_full_suite == 'true'
needs:
- info
@@ -889,13 +859,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -907,7 +877,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.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: pytest_buckets
path: pytest_buckets.txt
@@ -916,7 +886,11 @@ jobs:
pytest-full:
runs-on: ubuntu-24.04
if: |
needs.info.outputs.lint_only != 'true'
(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.test_full_suite == 'true'
needs:
- info
@@ -949,13 +923,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -968,7 +942,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: pytest_buckets
- name: Compile English translations
@@ -988,7 +962,6 @@ 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)"
@@ -1007,24 +980,18 @@ jobs:
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.6.2
uses: actions/upload-artifact@v4.6.0
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.6.2
uses: actions/upload-artifact@v4.6.0
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.2
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
@@ -1042,7 +1009,11 @@ jobs:
MYSQL_ROOT_PASSWORD: password
options: --health-cmd="mysqladmin ping -uroot -ppassword" --health-interval=5s --health-timeout=2s --health-retries=3
if: |
needs.info.outputs.lint_only != 'true'
(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.mariadb_groups != '[]'
needs:
- info
@@ -1074,13 +1045,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -1117,7 +1088,6 @@ 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 \
@@ -1138,7 +1108,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.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1146,19 +1116,12 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v4.6.0
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.2
with:
name: test-results-mariadb-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
path: junit.xml
- name: Check dirty
run: |
./script/check_dirty
@@ -1174,7 +1137,11 @@ jobs:
POSTGRES_PASSWORD: password
options: --health-cmd="pg_isready -hlocalhost -Upostgres" --health-interval=5s --health-timeout=2s --health-retries=3
if: |
needs.info.outputs.lint_only != 'true'
(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.postgresql_groups != '[]'
needs:
- info
@@ -1208,13 +1175,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -1251,7 +1218,6 @@ 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 \
@@ -1273,7 +1239,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.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1281,19 +1247,12 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v4.6.0
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.2
with:
name: test-results-postgres-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
path: junit.xml
- name: Check dirty
run: |
./script/check_dirty
@@ -1312,12 +1271,12 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.2
uses: codecov/codecov-action@v5.3.1
with:
fail_ci_if_error: true
flags: full-suite
@@ -1326,7 +1285,11 @@ jobs:
pytest-partial:
runs-on: ubuntu-24.04
if: |
needs.info.outputs.lint_only != 'true'
(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.tests_glob
&& needs.info.outputs.test_full_suite == 'false'
needs:
@@ -1359,13 +1322,13 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.6.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.3
uses: actions/cache/restore@v4.2.0
with:
path: venv
fail-on-cache-miss: true
@@ -1402,7 +1365,6 @@ 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 \
@@ -1420,24 +1382,18 @@ jobs:
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.6.2
uses: actions/upload-artifact@v4.6.0
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.6.2
uses: actions/upload-artifact@v4.6.0
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.2
with:
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
path: junit.xml
- name: Check dirty
run: |
./script/check_dirty
@@ -1454,37 +1410,12 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.2
uses: codecov/codecov-action@v5.3.1
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.3.0
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 }}
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.16
uses: github/codeql-action/init@v3.28.9
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.16
uses: github/codeql-action/analyze@v3.28.9
with:
category: "/language:python"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.6.0
uses: actions/setup-python@v5.4.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
+52 -16
View File
@@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.0
uses: actions/setup-python@v5.4.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -91,7 +91,7 @@ jobs:
) > build_constraints.txt
- name: Upload env_file
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: env_file
path: ./.env_file
@@ -99,14 +99,14 @@ jobs:
overwrite: true
- name: Upload build_constraints
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: build_constraints
path: ./build_constraints.txt
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -118,7 +118,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.6.2
uses: actions/upload-artifact@v4.6.0
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -138,17 +138,17 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: requirements_diff
@@ -159,7 +159,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
uses: home-assistant/wheels@2024.11.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -187,22 +187,22 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v4.1.8
with:
name: requirements_all_wheels
@@ -218,8 +218,16 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
- 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
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -230,4 +238,32 @@ jobs:
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.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-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.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-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.txtac"
-1
View File
@@ -69,7 +69,6 @@ test-reports/
test-results.xml
test-output.xml
pytest-*.txt
junit.xml
# Translations
*.mo
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
rev: v0.9.1
hooks:
- id: ruff
args:
-15
View File
@@ -103,7 +103,6 @@ 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,7 +118,6 @@ homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.*
homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.*
homeassistant.components.bosch_alarm.*
homeassistant.components.braviatv.*
homeassistant.components.bring.*
homeassistant.components.brother.*
@@ -137,7 +135,6 @@ 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.*
@@ -237,7 +234,6 @@ 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.*
@@ -291,7 +287,6 @@ homeassistant.components.kaleidescape.*
homeassistant.components.knocki.*
homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.kulersky.*
homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.*
homeassistant.components.lamarzocco.*
@@ -332,7 +327,6 @@ homeassistant.components.media_player.*
homeassistant.components.media_source.*
homeassistant.components.met_eireann.*
homeassistant.components.metoffice.*
homeassistant.components.miele.*
homeassistant.components.mikrotik.*
homeassistant.components.min_max.*
homeassistant.components.minecraft_server.*
@@ -364,10 +358,8 @@ homeassistant.components.no_ip.*
homeassistant.components.nordpool.*
homeassistant.components.notify.*
homeassistant.components.notion.*
homeassistant.components.ntfy.*
homeassistant.components.number.*
homeassistant.components.nut.*
homeassistant.components.ohme.*
homeassistant.components.onboarding.*
homeassistant.components.oncue.*
homeassistant.components.onedrive.*
@@ -387,7 +379,6 @@ homeassistant.components.pandora.*
homeassistant.components.panel_custom.*
homeassistant.components.peblar.*
homeassistant.components.peco.*
homeassistant.components.pegel_online.*
homeassistant.components.persistent_notification.*
homeassistant.components.person.*
homeassistant.components.pi_hole.*
@@ -403,7 +394,6 @@ 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.*
@@ -416,9 +406,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.remote_calendar.*
homeassistant.components.renault.*
homeassistant.components.reolink.*
homeassistant.components.repairs.*
@@ -449,7 +437,6 @@ 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.*
@@ -464,7 +451,6 @@ homeassistant.components.slack.*
homeassistant.components.sleepiq.*
homeassistant.components.smhi.*
homeassistant.components.smlight.*
homeassistant.components.smtp.*
homeassistant.components.snooz.*
homeassistant.components.solarlog.*
homeassistant.components.sonarr.*
@@ -538,7 +524,6 @@ 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.*
+2 -9
View File
@@ -38,17 +38,10 @@
"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/
@@ -84,4 +77,4 @@
]
}
]
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
{
"label": "Run Home Assistant Core",
"type": "shell",
"command": "${command:python.interpreterPath} -m homeassistant -c ./config",
"command": "hass -c ./config",
"group": "test",
"presentation": {
"reveal": "always",
Generated
+19 -41
View File
@@ -180,8 +180,6 @@ 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
@@ -216,8 +214,6 @@ build.json @home-assistant/supervisor
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
/tests/components/bosch_alarm/ @mag1024 @sanjay900
/homeassistant/components/bosch_shc/ @tschamm
/tests/components/bosch_shc/ @tschamm
/homeassistant/components/braviatv/ @bieniu @Drafteed
@@ -432,7 +428,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50 @roberty99
/homeassistant/components/ephember/ @ttroy50
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
/tests/components/epic_games_store/ @hacf-fr @Quentame
/homeassistant/components/epion/ @lhgravendeel
@@ -572,8 +568,8 @@ build.json @home-assistant/supervisor
/tests/components/google_cloud/ @lufton @tronikos
/homeassistant/components/google_drive/ @tronikos
/tests/components/google_drive/ @tronikos
/homeassistant/components/google_generative_ai_conversation/ @tronikos @ivanlh
/tests/components/google_generative_ai_conversation/ @tronikos @ivanlh
/homeassistant/components/google_generative_ai_conversation/ @tronikos
/tests/components/google_generative_ai_conversation/ @tronikos
/homeassistant/components/google_mail/ @tkdrob
/tests/components/google_mail/ @tkdrob
/homeassistant/components/google_photos/ @allenporter
@@ -704,8 +700,6 @@ build.json @home-assistant/supervisor
/tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh
/homeassistant/components/imeon_inverter/ @Imeon-Energy
/tests/components/imeon_inverter/ @Imeon-Energy
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/improv_ble/ @emontnemery
@@ -937,8 +931,6 @@ build.json @home-assistant/supervisor
/tests/components/metoffice/ @MrHarcombe @avee87
/homeassistant/components/microbees/ @microBeesTech
/tests/components/microbees/ @microBeesTech
/homeassistant/components/miele/ @astrandb
/tests/components/miele/ @astrandb
/homeassistant/components/mikrotik/ @engrbm87
/tests/components/mikrotik/ @engrbm87
/homeassistant/components/mill/ @danielhiversen
@@ -975,8 +967,8 @@ build.json @home-assistant/supervisor
/tests/components/motionblinds_ble/ @LennP @jerrybboy
/homeassistant/components/motioneye/ @dermotduffy
/tests/components/motioneye/ @dermotduffy
/homeassistant/components/motionmount/ @laiho-vogels
/tests/components/motionmount/ @laiho-vogels
/homeassistant/components/motionmount/ @RJPoelstra
/tests/components/motionmount/ @RJPoelstra
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco
/homeassistant/components/msteams/ @peroyvind
@@ -1051,8 +1043,6 @@ build.json @home-assistant/supervisor
/tests/components/nsw_fuel_station/ @nickw444
/homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte
/tests/components/nsw_rural_fire_service_feed/ @exxamalte
/homeassistant/components/ntfy/ @tr4nt0r
/tests/components/ntfy/ @tr4nt0r
/homeassistant/components/nuheat/ @tstabrawa
/tests/components/nuheat/ @tstabrawa
/homeassistant/components/nuki/ @pschmitt @pvizeli @pree
@@ -1061,8 +1051,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 @tdfountain
/tests/components/nut/ @bdraco @ollo69 @pestevez @tdfountain
/homeassistant/components/nut/ @bdraco @ollo69 @pestevez
/tests/components/nut/ @bdraco @ollo69 @pestevez
/homeassistant/components/nws/ @MatthewFlamm @kamiyo
/tests/components/nws/ @MatthewFlamm @kamiyo
/homeassistant/components/nyt_games/ @joostlek
@@ -1081,6 +1071,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/ombi/ @larssont
/homeassistant/components/onboarding/ @home-assistant/core
/tests/components/onboarding/ @home-assistant/core
/homeassistant/components/oncue/ @bdraco @peterager
/tests/components/oncue/ @bdraco @peterager
/homeassistant/components/ondilo_ico/ @JeromeHXP
/tests/components/ondilo_ico/ @JeromeHXP
/homeassistant/components/onedrive/ @zweckj
@@ -1152,8 +1144,8 @@ build.json @home-assistant/supervisor
/tests/components/philips_js/ @elupus
/homeassistant/components/pi_hole/ @shenxn
/tests/components/pi_hole/ @shenxn
/homeassistant/components/picnic/ @corneyl @codesalatdev
/tests/components/picnic/ @corneyl @codesalatdev
/homeassistant/components/picnic/ @corneyl
/tests/components/picnic/ @corneyl
/homeassistant/components/ping/ @jpbede
/tests/components/ping/ @jpbede
/homeassistant/components/plaato/ @JohNan
@@ -1189,8 +1181,6 @@ build.json @home-assistant/supervisor
/tests/components/prusalink/ @balloob
/homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45
/homeassistant/components/pterodactyl/ @elmurato
/tests/components/pterodactyl/ @elmurato
/homeassistant/components/pure_energie/ @klaasnicolaas
/tests/components/pure_energie/ @klaasnicolaas
/homeassistant/components/purpleair/ @bachya
@@ -1258,12 +1248,8 @@ build.json @home-assistant/supervisor
/tests/components/recovery_mode/ @home-assistant/core
/homeassistant/components/refoss/ @ashionky
/tests/components/refoss/ @ashionky
/homeassistant/components/rehlko/ @bdraco @peterager
/tests/components/rehlko/ @bdraco @peterager
/homeassistant/components/remote/ @home-assistant/core
/tests/components/remote/ @home-assistant/core
/homeassistant/components/remote_calendar/ @Thomas55555
/tests/components/remote_calendar/ @Thomas55555
/homeassistant/components/renault/ @epenet
/tests/components/renault/ @epenet
/homeassistant/components/renson/ @jimmyd-be
@@ -1318,8 +1304,6 @@ build.json @home-assistant/supervisor
/tests/components/ruuvitag_ble/ @akx
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
/homeassistant/components/s3/ @tomasbedrich
/tests/components/s3/ @tomasbedrich
/homeassistant/components/sabnzbd/ @shaiu @jpbede
/tests/components/sabnzbd/ @shaiu @jpbede
/homeassistant/components/saj/ @fredericvl
@@ -1358,8 +1342,6 @@ 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
@@ -1395,6 +1377,7 @@ build.json @home-assistant/supervisor
/homeassistant/components/siren/ @home-assistant/core @raman325
/tests/components/siren/ @home-assistant/core @raman325
/homeassistant/components/sisyphus/ @jkeljo
/homeassistant/components/sky_hub/ @rogerselwyn
/homeassistant/components/sky_remote/ @dunnmj @saty9
/tests/components/sky_remote/ @dunnmj @saty9
/homeassistant/components/skybell/ @tkdrob
@@ -1414,8 +1397,6 @@ build.json @home-assistant/supervisor
/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
@@ -1430,8 +1411,6 @@ 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
@@ -1441,8 +1420,8 @@ build.json @home-assistant/supervisor
/tests/components/solarlog/ @Ernst79 @dontinelli
/homeassistant/components/solax/ @squishykid @Darsstar
/tests/components/solax/ @squishykid @Darsstar
/homeassistant/components/soma/ @ratsept
/tests/components/soma/ @ratsept
/homeassistant/components/soma/ @ratsept @sebfortier2288
/tests/components/soma/ @ratsept @sebfortier2288
/homeassistant/components/sonarr/ @ctalkington
/tests/components/sonarr/ @ctalkington
/homeassistant/components/songpal/ @rytilahti @shenxn
@@ -1474,8 +1453,7 @@ build.json @home-assistant/supervisor
/tests/components/steam_online/ @tkdrob
/homeassistant/components/steamist/ @bdraco
/tests/components/steamist/ @bdraco
/homeassistant/components/stiebel_eltron/ @fucm @ThyMYthOS
/tests/components/stiebel_eltron/ @fucm @ThyMYthOS
/homeassistant/components/stiebel_eltron/ @fucm
/homeassistant/components/stookwijzer/ @fwestenberg
/tests/components/stookwijzer/ @fwestenberg
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
@@ -1488,6 +1466,8 @@ build.json @home-assistant/supervisor
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/sunweg/ @rokam
/tests/components/sunweg/ @rokam
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen
@@ -1541,8 +1521,8 @@ build.json @home-assistant/supervisor
/tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike
/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core
/homeassistant/components/template/ @PhracturedBlue @home-assistant/core
/tests/components/template/ @PhracturedBlue @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77
/tests/components/tesla_fleet/ @Bre77
/homeassistant/components/tesla_wall_connector/ @einarhauks
@@ -1711,8 +1691,6 @@ 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
Generated
+19 -19
View File
@@ -12,26 +12,8 @@ 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.7.1
RUN pip3 install uv==0.5.27
WORKDIR /usr/src
@@ -60,4 +42,22 @@ 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.8/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
&& go2rtc --version
WORKDIR /config
+6 -6
View File
@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
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
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.12.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.12.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.12.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.12.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.12.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
@@ -19,4 +19,4 @@ labels:
org.opencontainers.image.authors: The Home Assistant Authors
org.opencontainers.image.url: https://www.home-assistant.io/
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
org.opencontainers.image.licenses: Apache-2.0
org.opencontainers.image.licenses: Apache License 2.0
-9
View File
@@ -178,15 +178,6 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=SSLContext.set_default_verify_paths,
object=SSLContext,
function="set_default_verify_paths",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.open,
object=Path,
+246 -210
View File
@@ -53,7 +53,6 @@ from .components import (
logbook as logbook_pre_import, # noqa: F401
lovelace as lovelace_pre_import, # noqa: F401
onboarding as onboarding_pre_import, # noqa: F401
person as person_pre_import, # noqa: F401
recorder as recorder_import, # noqa: F401 - not named pre_import since it has requirements
repairs as repairs_pre_import, # noqa: F401
search as search_pre_import, # noqa: F401
@@ -75,14 +74,12 @@ 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,
@@ -94,7 +91,6 @@ 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
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
@@ -138,12 +134,14 @@ 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"}
@@ -154,10 +152,6 @@ 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",
@@ -167,28 +161,23 @@ FRONTEND_INTEGRATIONS = {
# integrations can be removed and database migration status is
# visible in frontend
"frontend",
# Hassio is an after dependency of backup, after dependencies
# are not promoted from stage 2 to earlier stages, so we need to
# add it here. Hassio needs to be setup before backup, otherwise
# the backup integration will think we are a container/core install
# when using HAOS or Supervised install.
"hassio",
# Backup is an after dependency of frontend, after dependencies
# are not promoted from stage 2 to earlier stages, so we need to
# add it here.
"backup",
}
# 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.
RECORDER_INTEGRATIONS = {
# Setup after frontend
# To record data
"recorder",
}
DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb", "zeroconf")
STAGE_1_INTEGRATIONS = {
# We need to make sure discovery integrations
# update their deps before stage 2 integrations
@@ -203,7 +192,6 @@ STAGE_1_INTEGRATIONS = {
# Ensure supervisor is available
"hassio",
}
DEFAULT_INTEGRATIONS = {
# These integrations are set up unless recovery mode is activated.
#
@@ -244,12 +232,22 @@ 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.
@@ -301,6 +299,14 @@ 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:
@@ -316,10 +322,10 @@ async def async_setup_hass(
block_async_io.enable()
if not (recovery_mode := runtime_config.recovery_mode):
config_dict = None
basic_setup_success = False
config_dict = None
basic_setup_success = False
if not (recovery_mode := runtime_config.recovery_mode):
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
try:
@@ -337,43 +343,39 @@ 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 hass.async_stop(force=True)
hass = await create_hass()
if config_dict is None:
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 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 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 hass.async_stop(force=True)
hass = await create_hass()
recovery_mode = True
await stop_hass(hass)
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")
@@ -436,10 +438,9 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
if DATA_REGISTRIES_LOADED in hass.data:
return
hass.data[DATA_REGISTRIES_LOADED] = None
entity.async_setup(hass)
frame.async_setup(hass)
template.async_setup(hass)
translation.async_setup(hass)
entity.async_setup(hass)
template.async_setup(hass)
await asyncio.gather(
create_eager_task(get_internal_store_manager(hass).async_initialize()),
create_eager_task(area_registry.async_load(hass)),
@@ -660,10 +661,11 @@ 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
@@ -692,6 +694,7 @@ 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]
@@ -713,25 +716,20 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
return domains
async def _async_resolve_domains_and_preload(
async def _async_resolve_domains_to_setup(
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.
"""
) -> 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 `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.
# 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:
@@ -747,78 +745,111 @@ async def _async_resolve_domains_and_preload(
# 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 = {
# 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
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
}
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()
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
)
)
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
# 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()))
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 it's an optimization only
# We do not wait for this since its an optimization only
hass.async_create_background_task(
requirements.async_load_installed_versions(hass, all_requirements),
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
@@ -829,7 +860,6 @@ async def _async_resolve_domains_and_preload(
# 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",
@@ -841,13 +871,13 @@ async def _async_resolve_domains_and_preload(
# in the setup process.
hass.async_create_background_task(
get_internal_store_manager(hass).async_preload(
[*PRELOAD_STORAGE, *all_integrations_to_setup]
[*PRELOAD_STORAGE, *domains_to_setup]
),
"preload storage",
eager_start=True,
)
return integrations_to_setup, all_integrations_to_setup
return domains_to_setup, integration_cache
async def _async_set_up_integrations(
@@ -857,84 +887,86 @@ async def _async_set_up_integrations(
watcher = _WatchPendingSetups(hass, _setup_started(hass))
watcher.async_start()
integrations, all_integrations = await _async_resolve_domains_and_preload(
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
hass, config
)
# Detect all cycles
integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, all_integrations.values(), set(all_integrations)
)
)
all_domains = set(integrations_after_dependencies)
domains = set(integrations) & all_domains
_LOGGER.info(
"Domains to be set up: %s | %s",
domains,
all_domains - domains,
)
async_set_domains_to_be_loaded(hass, all_domains)
# Initialize recorder
if "recorder" in all_domains:
if "recorder" in domains_to_setup:
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),
pre_stage_domains = [
(name, domains_to_setup & domain_group) for name, domain_group in SETUP_ORDER
]
_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
# calculate what components to setup in what stage
stage_1_domains: set[str] = set()
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
# 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()
stage_dep_domains_unfiltered = {
dep
for domain in stage_domains
for dep in integrations_after_dependencies[domain]
if dep not in stage_domains
}
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
for domain in old_deps_promotion:
if domain not in domains_to_setup or domain in stage_1_domains:
continue
stage_all_domains = stage_domains | stage_dep_domains
stage_1_domains.add(domain)
_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,
)
if (dep_itg := integration_cache.get(domain)) is None:
continue
if timeout is None:
await _async_setup_multi_components(hass, stage_all_domains, config)
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(timeout, cool_down=COOLDOWN_TIME):
await _async_setup_multi_components(hass, stage_all_domains, config)
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 %s waiting on %s - moving forward",
name,
"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
)
@@ -1036,6 +1068,8 @@ async def _async_setup_multi_components(
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.
@@ -1045,7 +1079,9 @@ async def _async_setup_multi_components(
f"setup component {domain}",
eager_start=True,
)
for domain in sorted(domains, key=SETUP_ORDER_SORT_KEY, reverse=True)
for domain in sorted(
domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True
)
}
results = await asyncio.gather(*futures.values(), return_exceptions=True)
for idx, domain in enumerate(futures):
-5
View File
@@ -1,5 +0,0 @@
{
"domain": "bosch",
"name": "Bosch",
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
}
-5
View File
@@ -1,5 +0,0 @@
{
"domain": "eve",
"name": "Eve",
"iot_standards": ["matter"]
}
-1
View File
@@ -6,7 +6,6 @@
"google_assistant_sdk",
"google_cloud",
"google_drive",
"google_gemini",
"google_generative_ai_conversation",
"google_mail",
"google_maps",
-1
View File
@@ -6,7 +6,6 @@
"azure_devops",
"azure_event_hub",
"azure_service_bus",
"azure_storage",
"microsoft_face_detect",
"microsoft_face_identify",
"microsoft_face",
+1 -2
View File
@@ -1,6 +1,5 @@
{
"domain": "motionblinds",
"name": "Motionblinds",
"integrations": ["motion_blinds", "motionblinds_ble"],
"iot_standards": ["matter"]
"integrations": ["motion_blinds", "motionblinds_ble"]
}
-6
View File
@@ -1,6 +0,0 @@
{
"domain": "nuki",
"name": "Nuki",
"integrations": ["nuki"],
"iot_standards": ["matter"]
}
-5
View File
@@ -1,5 +0,0 @@
{
"domain": "sensorpush",
"name": "SensorPush",
"integrations": ["sensorpush", "sensorpush_cloud"]
}
+1 -14
View File
@@ -24,7 +24,7 @@ from homeassistant.components.weather import (
API_METRIC: Final = "Metric"
ATTRIBUTION: Final = "Data provided by AccuWeather"
ATTR_CATEGORY_VALUE = "CategoryValue"
ATTR_CATEGORY: Final = "Category"
ATTR_DIRECTION: Final = "Direction"
ATTR_ENGLISH: Final = "English"
ATTR_LEVEL: Final = "level"
@@ -55,18 +55,5 @@ 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)
@@ -75,11 +75,7 @@ class AccuWeatherObservationDataUpdateCoordinator(
async with timeout(10):
result = await self.accuweather.async_get_current_conditions()
except EXCEPTIONS as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="current_conditions_update_error",
translation_placeholders={"error": repr(error)},
) from error
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
@@ -121,15 +117,9 @@ class AccuWeatherDailyForecastDataUpdateCoordinator(
"""Update data via library."""
try:
async with timeout(10):
result = await self.accuweather.async_get_daily_forecast(
language=self.hass.config.language
)
result = await self.accuweather.async_get_daily_forecast()
except EXCEPTIONS as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="forecast_update_error",
translation_placeholders={"error": repr(error)},
) from error
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==4.2.0"],
"requirements": ["accuweather==4.0.0"],
"single_config_entry": true
}
+8 -20
View File
@@ -29,9 +29,8 @@ 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_VALUE,
ATTR_CATEGORY,
ATTR_DIRECTION,
ATTR_ENGLISH,
ATTR_LEVEL,
@@ -39,7 +38,6 @@ from .const import (
ATTR_VALUE,
ATTRIBUTION,
MAX_FORECAST_DAYS,
POLLEN_CATEGORY_MAP,
)
from .coordinator import (
AccuWeatherConfigEntry,
@@ -61,9 +59,9 @@ class AccuWeatherSensorDescription(SensorEntityDescription):
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
AccuWeatherSensorDescription(
key="AirQuality",
value_fn=lambda data: AIR_QUALITY_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]],
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
device_class=SensorDeviceClass.ENUM,
options=list(AIR_QUALITY_CATEGORY_MAP.values()),
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
translation_key="air_quality",
),
AccuWeatherSensorDescription(
@@ -85,9 +83,7 @@ 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: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
},
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="grass_pollen",
),
AccuWeatherSensorDescription(
@@ -111,9 +107,7 @@ 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: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
},
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="mold_pollen",
),
AccuWeatherSensorDescription(
@@ -121,9 +115,7 @@ 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: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
},
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="ragweed_pollen",
),
AccuWeatherSensorDescription(
@@ -189,18 +181,14 @@ 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: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
},
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
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: POLLEN_CATEGORY_MAP[data[ATTR_CATEGORY_VALUE]]
},
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="uv_index_forecast",
),
AccuWeatherSensorDescription(
@@ -26,20 +26,10 @@
"state": {
"good": "Good",
"hazardous": "Hazardous",
"high": "High",
"low": "Low",
"moderate": "Moderate",
"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%]"
}
}
"unhealthy": "Unhealthy"
}
},
"apparent_temperature": {
@@ -72,10 +62,12 @@
"level": {
"name": "Level",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "Moderate",
"very_high": "[%key:common::state::very_high%]"
"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%]"
}
}
}
@@ -89,10 +81,12 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:common::state::very_high%]"
"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%]"
}
}
}
@@ -106,15 +100,6 @@
"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": {
@@ -123,10 +108,12 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:common::state::very_high%]"
"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%]"
}
}
}
@@ -167,10 +154,12 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:common::state::very_high%]"
"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%]"
}
}
}
@@ -181,10 +170,12 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:common::state::very_high%]"
"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%]"
}
}
}
@@ -195,10 +186,12 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:common::state::very_high%]"
"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%]"
}
}
}
@@ -229,14 +222,6 @@
}
}
},
"exceptions": {
"current_conditions_update_error": {
"message": "An error occurred while retrieving weather current conditions data from the AccuWeather API: {error}"
},
"forecast_update_error": {
"message": "An error occurred while retrieving weather forecast data from the AccuWeather API: {error}"
}
},
"system_health": {
"info": {
"can_reach_server": "Reach AccuWeather server",
+4 -17
View File
@@ -2,38 +2,25 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import CONNECTION_TYPE, LOCAL
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
PLATFORMS = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Adax from a config entry."""
if entry.data.get(CONNECTION_TYPE) == LOCAL:
local_coordinator = AdaxLocalCoordinator(hass, entry)
entry.runtime_data = local_coordinator
else:
cloud_coordinator = AdaxCloudCoordinator(hass, entry)
entry.runtime_data = cloud_coordinator
await entry.runtime_data.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
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_migrate_entry(
hass: HomeAssistant, config_entry: AdaxConfigEntry
) -> bool:
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
# convert title and unique_id to string
if config_entry.version == 1:
+68 -79
View File
@@ -12,42 +12,57 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_TOKEN,
CONF_UNIQUE_ID,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
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 AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AdaxConfigEntry
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
from .coordinator import AdaxCloudCoordinator, AdaxLocalCoordinator
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL
async def async_setup_entry(
hass: HomeAssistant,
entry: AdaxConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Adax thermostat with config flow."""
if entry.data.get(CONNECTION_TYPE) == LOCAL:
local_coordinator = cast(AdaxLocalCoordinator, entry.runtime_data)
async_add_entities(
[LocalAdaxDevice(local_coordinator, entry.data[CONF_UNIQUE_ID])],
adax_data_handler = AdaxLocal(
entry.data[CONF_IP_ADDRESS],
entry.data[CONF_TOKEN],
websession=async_get_clientsession(hass, verify_ssl=False),
)
else:
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
async_add_entities(
AdaxDevice(cloud_coordinator, device_id)
for device_id in cloud_coordinator.data
[LocalAdaxDevice(adax_data_handler, entry.data[CONF_UNIQUE_ID])], True
)
return
adax_data_handler = Adax(
entry.data[ACCOUNT_ID],
entry.data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
)
async_add_entities(
(
AdaxDevice(room, adax_data_handler)
for room in await adax_data_handler.get_rooms()
),
True,
)
class AdaxDevice(CoordinatorEntity[AdaxCloudCoordinator], ClimateEntity):
class AdaxDevice(ClimateEntity):
"""Representation of a heater."""
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
@@ -61,37 +76,20 @@ class AdaxDevice(CoordinatorEntity[AdaxCloudCoordinator], ClimateEntity):
_attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(
self,
coordinator: AdaxCloudCoordinator,
device_id: str,
) -> None:
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
"""Initialize the heater."""
super().__init__(coordinator)
self._adax_data_handler: Adax = coordinator.adax_data_handler
self._device_id = device_id
self._device_id = heater_data["id"]
self._adax_data_handler = adax_data_handler
self._attr_name = self.room["name"]
self._attr_unique_id = f"{self.room['homeId']}_{self._device_id}"
self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
identifiers={(DOMAIN, heater_data["id"])},
# Instead of setting the device name to the entity name, adax
# should be updated to set has_entity_name = True, and set the entity
# name to None
name=cast(str | None, self.name),
manufacturer="Adax",
)
self._apply_data(self.room)
@property
def available(self) -> bool:
"""Whether the entity is available or not."""
return super().available and self._device_id in self.coordinator.data
@property
def room(self) -> dict[str, Any]:
"""Gets the data for this particular device."""
return self.coordinator.data[self._device_id]
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode."""
@@ -106,9 +104,7 @@ class AdaxDevice(CoordinatorEntity[AdaxCloudCoordinator], ClimateEntity):
)
else:
return
# Request data refresh from source to verify that update was successful
await self.coordinator.async_request_refresh()
await self._adax_data_handler.update()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
@@ -118,31 +114,28 @@ class AdaxDevice(CoordinatorEntity[AdaxCloudCoordinator], ClimateEntity):
self._device_id, temperature, True
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if room := self.room:
self._apply_data(room)
super()._handle_coordinator_update()
def _apply_data(self, room: dict[str, Any]) -> None:
"""Update the appropriate attributues based on received data."""
self._attr_current_temperature = room.get("temperature")
self._attr_target_temperature = room.get("targetTemperature")
if room["heatingEnabled"]:
self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator"
else:
self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off"
async def async_update(self) -> None:
"""Get the latest data."""
for room in await self._adax_data_handler.get_rooms():
if room["id"] != self._device_id:
continue
self._attr_name = room["name"]
self._attr_current_temperature = room.get("temperature")
self._attr_target_temperature = room.get("targetTemperature")
if room["heatingEnabled"]:
self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator"
else:
self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off"
return
class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
class LocalAdaxDevice(ClimateEntity):
"""Representation of a heater."""
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_hvac_mode = HVACMode.OFF
_attr_icon = "mdi:radiator-off"
_attr_hvac_mode = HVACMode.HEAT
_attr_max_temp = 35
_attr_min_temp = 5
_attr_supported_features = (
@@ -153,10 +146,9 @@ class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
_attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(self, coordinator: AdaxLocalCoordinator, unique_id: str) -> None:
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None:
"""Initialize the heater."""
super().__init__(coordinator)
self._adax_data_handler: AdaxLocal = coordinator.adax_data_handler
self._adax_data_handler = adax_data_handler
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
@@ -177,20 +169,17 @@ class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
return
await self._adax_data_handler.set_target_temperature(temperature)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if data := self.coordinator.data:
self._attr_current_temperature = data["current_temperature"]
self._attr_available = self._attr_current_temperature is not None
if (target_temp := data["target_temperature"]) == 0:
self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off"
if target_temp == 0:
self._attr_target_temperature = self._attr_min_temp
else:
self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator"
self._attr_target_temperature = target_temp
super()._handle_coordinator_update()
async def async_update(self) -> None:
"""Get the latest data."""
data = await self._adax_data_handler.get_status()
self._attr_current_temperature = data["current_temperature"]
self._attr_available = self._attr_current_temperature is not None
if (target_temp := data["target_temperature"]) == 0:
self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off"
if target_temp == 0:
self._attr_target_temperature = self._attr_min_temp
else:
self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator"
self._attr_target_temperature = target_temp
-3
View File
@@ -1,6 +1,5 @@
"""Constants for the Adax integration."""
import datetime
from typing import Final
ACCOUNT_ID: Final = "account_id"
@@ -10,5 +9,3 @@ DOMAIN: Final = "adax"
LOCAL = "Local"
WIFI_SSID = "wifi_ssid"
WIFI_PSWD = "wifi_pswd"
SCAN_INTERVAL = datetime.timedelta(seconds=60)
@@ -1,71 +0,0 @@
"""DataUpdateCoordinator for the Adax component."""
import logging
from typing import Any, cast
from adax import Adax
from adax_local import Adax as AdaxLocal
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ACCOUNT_ID, SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
type AdaxConfigEntry = ConfigEntry[AdaxCloudCoordinator | AdaxLocalCoordinator]
class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
"""Coordinator for updating data to and from Adax (cloud)."""
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
"""Initialize the Adax coordinator used for Cloud mode."""
super().__init__(
hass,
config_entry=entry,
logger=_LOGGER,
name="AdaxCloud",
update_interval=SCAN_INTERVAL,
)
self.adax_data_handler = Adax(
entry.data[ACCOUNT_ID],
entry.data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
)
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch data from the Adax."""
rooms = await self.adax_data_handler.get_rooms() or []
return {r["id"]: r for r in rooms}
class AdaxLocalCoordinator(DataUpdateCoordinator[dict[str, Any] | None]):
"""Coordinator for updating data to and from Adax (local)."""
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
"""Initialize the Adax coordinator used for Local mode."""
super().__init__(
hass,
config_entry=entry,
logger=_LOGGER,
name="AdaxLocal",
update_interval=SCAN_INTERVAL,
)
self.adax_data_handler = AdaxLocal(
entry.data[CONF_IP_ADDRESS],
entry.data[CONF_TOKEN],
websession=async_get_clientsession(hass, verify_ssl=False),
)
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from the Adax."""
if result := await self.adax_data_handler.get_status():
return cast(dict[str, Any], result)
raise UpdateFailed("Got invalid status from device")
+3 -3
View File
@@ -5,14 +5,14 @@
"data": {
"connection_type": "Select connection type"
},
"description": "Select connection type. Local requires heaters with Bluetooth"
"description": "Select connection type. Local requires heaters with bluetooth"
},
"local": {
"data": {
"wifi_ssid": "Wi-Fi SSID",
"wifi_pswd": "Wi-Fi password"
"wifi_pswd": "Wi-Fi Password"
},
"description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue LED starts blinking before pressing Submit. Configuring heater might take some minutes."
"description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue led starts blinking before pressing Submit. Configuring heater might take some minutes."
},
"cloud": {
"data": {
+7 -2
View File
@@ -7,7 +7,7 @@ from dataclasses import dataclass
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
@@ -123,7 +123,12 @@ 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)
if not hass.config_entries.async_loaded_entries(DOMAIN):
loaded_entries = [
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]
if len(loaded_entries) == 1:
# 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)
@@ -2,7 +2,6 @@
from __future__ import annotations
from decimal import Decimal
import logging
from typing import Any
@@ -15,7 +14,6 @@ from homeassistant.components.climate import (
FAN_MEDIUM,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
@@ -51,14 +49,6 @@ 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,
@@ -185,17 +175,6 @@ 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."""
@@ -294,22 +273,6 @@ 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,4 +7,3 @@ 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"
@@ -41,7 +41,7 @@ async def async_setup_entry(
entities.append(
AdvantageAirThingCover(instance, thing, CoverDeviceClass.BLIND)
)
elif thing["channelDipState"] in [3, 10]: # 3 & 10 = "Garage door"
elif thing["channelDipState"] == 3: # 3 = "Garage door"
entities.append(
AdvantageAirThingCover(instance, thing, CoverDeviceClass.GARAGE)
)
@@ -9,7 +9,6 @@ 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,
)
@@ -33,8 +32,6 @@ 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)
@@ -96,32 +93,6 @@ 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."""
@@ -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 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."
"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."
}
}
}
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.9.2"],
"requirements": ["airgradient==0.9.1"],
"zeroconf": ["_airgradient._tcp.local."]
}
@@ -11,7 +11,7 @@
}
},
"discovery_confirm": {
"description": "Do you want to set up {model}?"
"description": "Do you want to setup {model}?"
}
},
"abort": {
@@ -68,8 +68,8 @@
"led_bar_mode": {
"name": "LED bar mode",
"state": {
"off": "[%key:common::state::off%]",
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"off": "Off",
"co2": "Carbon dioxide",
"pm": "Particulate matter"
}
},
@@ -143,8 +143,8 @@
"led_bar_mode": {
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
"state": {
"off": "[%key:common::state::off%]",
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
}
},
+2 -13
View File
@@ -105,14 +105,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | i
try:
await measurements.update()
except (AirlyError, ClientConnectorError) as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={
"entry": self.config_entry.title,
"error": repr(error),
},
) from error
raise UpdateFailed(error) from error
_LOGGER.debug(
"Requests remaining: %s/%s",
@@ -133,11 +126,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | i
standards = measurements.current["standards"]
if index["description"] == NO_AIRLY_SENSORS:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="no_station",
translation_placeholders={"entry": self.config_entry.title},
)
raise UpdateFailed("Can't retrieve data: no Airly sensors in this area")
for value in values:
data[value["name"]] = value["value"]
for standard in standards:
@@ -36,13 +36,5 @@
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
}
}
},
"exceptions": {
"update_error": {
"message": "An error occurred while retrieving data from the Airly API for {entry}: {error}"
},
"no_station": {
"message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area"
}
}
}
@@ -8,7 +8,7 @@ from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from pyairnow import WebServiceAPI
from pyairnow.conv import aqi_to_concentration
from pyairnow.errors import AirNowError, InvalidJsonError
from pyairnow.errors import AirNowError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
distance=self.distance,
)
except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
except (AirNowError, ClientConnectorError) as error:
raise UpdateFailed(error) from error
if not obs:
+2 -2
View File
@@ -7,7 +7,7 @@
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"radius": "Station radius (miles; optional)"
"radius": "Station Radius (miles; optional)"
}
}
},
@@ -25,7 +25,7 @@
"step": {
"init": {
"data": {
"radius": "Station radius (miles)"
"radius": "Station Radius (miles)"
}
}
}
@@ -83,7 +83,6 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(device_info["id"])
self._abort_if_unique_id_configured()
_LOGGER.debug("Creating an entry for %s", device_info["name"])
return self.async_create_entry(title=device_info["name"], data=user_input)
return self.async_show_form(
+2 -14
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from aioairq.core import AirQ, identify_warming_up_sensors
from aioairq import AirQ
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
@@ -55,9 +55,6 @@ class AirQCoordinator(DataUpdateCoordinator):
async def _async_update_data(self) -> dict:
"""Fetch the data from the device."""
if "name" not in self.device_info:
_LOGGER.debug(
"'name' not found in AirQCoordinator.device_info, fetching from the device"
)
info = await self.airq.fetch_device_info()
self.device_info.update(
DeviceInfo(
@@ -67,16 +64,7 @@ class AirQCoordinator(DataUpdateCoordinator):
hw_version=info["hw_version"],
)
)
_LOGGER.debug(
"Updated AirQCoordinator.device_info for 'name' %s",
self.device_info.get("name"),
)
data: dict = await self.airq.get_latest_data(
return await self.airq.get_latest_data( # type: ignore[no-any-return]
return_average=self.return_average,
clip_negative_values=self.clip_negative,
)
if warming_up_sensors := identify_warming_up_sensors(data):
_LOGGER.debug(
"Following sensors are still warming up: %s", warming_up_sensors
)
return data
+7 -7
View File
@@ -91,7 +91,7 @@
"name": "Hydrogen fluoride"
},
"health_index": {
"name": "Health index"
"name": "Health Index"
},
"absolute_humidity": {
"name": "Absolute humidity"
@@ -112,10 +112,10 @@
"name": "Oxygen"
},
"performance_index": {
"name": "Performance index"
"name": "Performance Index"
},
"hydrogen_phosphide": {
"name": "Hydrogen phosphide"
"name": "Hydrogen Phosphide"
},
"relative_pressure": {
"name": "Relative pressure"
@@ -127,22 +127,22 @@
"name": "Refrigerant"
},
"silicon_hydride": {
"name": "Silicon hydride"
"name": "Silicon Hydride"
},
"noise": {
"name": "Noise"
},
"maximum_noise": {
"name": "Noise (maximum)"
"name": "Noise (Maximum)"
},
"radon": {
"name": "Radon"
},
"industrial_volatile_organic_compounds": {
"name": "VOCs (industrial)"
"name": "VOCs (Industrial)"
},
"virus_index": {
"name": "Virus index"
"name": "Virus Index"
}
}
}
@@ -102,8 +102,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect")
except Exception:
_LOGGER.exception("Unknown error occurred")
except Exception: # noqa: BLE001
return self.async_abort(reason="unknown")
name = get_name(device)
@@ -161,8 +160,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect")
except Exception:
_LOGGER.exception("Unknown error occurred")
except Exception: # noqa: BLE001
return self.async_abort(reason="unknown")
name = get_name(device)
self._discovered_devices[address] = Discovery(name, discovery_info, device)
@@ -32,8 +32,7 @@ class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN):
client = Airtouch5SimpleClient(user_input[CONF_HOST])
try:
await client.test_connection()
except Exception:
_LOGGER.exception("Unexpected exception")
except Exception: # noqa: BLE001
errors = {"base": "cannot_connect"}
else:
await self.async_set_unique_id(user_input[CONF_HOST])
@@ -2,7 +2,7 @@
"config": {
"step": {
"geography_by_coords": {
"title": "Configure a geography",
"title": "Configure a Geography",
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
@@ -16,8 +16,8 @@
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"city": "City",
"state": "State",
"country": "[%key:common::config_flow::data::country%]"
"country": "Country",
"state": "State"
}
},
"reauth_confirm": {
@@ -56,12 +56,12 @@
"sensor": {
"pollutant_label": {
"state": {
"co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
"n2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
"o3": "[%key:component::sensor::entity_component::ozone::name%]",
"p1": "[%key:component::sensor::entity_component::pm10::name%]",
"p2": "[%key:component::sensor::entity_component::pm25::name%]",
"s2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
"co": "Carbon Monoxide",
"n2": "Nitrogen Dioxide",
"o3": "Ozone",
"p1": "PM10",
"p2": "PM2.5",
"s2": "Sulfur Dioxide"
}
},
"pollutant_level": {
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.0"]
"requirements": ["aioairzone==0.9.9"]
}
@@ -9,8 +9,6 @@ from aioairzone.const import (
AZD_HUMIDITY,
AZD_TEMP,
AZD_TEMP_UNIT,
AZD_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_SIGNAL,
AZD_WEBSERVER,
AZD_WIFI_RSSI,
AZD_ZONES,
@@ -75,20 +73,6 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
device_class=SensorDeviceClass.BATTERY,
key=AZD_THERMOSTAT_BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_THERMOSTAT_SIGNAL,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="thermostat_signal",
),
)
@@ -76,9 +76,6 @@
"sensor": {
"rssi": {
"name": "RSSI"
},
"thermostat_signal": {
"name": "Signal strength"
}
}
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.12"]
"requirements": ["aioairzone-cloud==0.6.10"]
}
@@ -32,9 +32,9 @@
"air_quality": {
"name": "Air Quality mode",
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"auto": "[%key:common::state::auto%]"
"off": "Off",
"on": "On",
"auto": "Auto"
}
},
"modes": {
@@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
@@ -28,13 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return True
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove a config entry."""
if not hass.config_entries.async_loaded_entries(DOMAIN):
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
# Remove any remaining disabled or ignored entries
for _entry in hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
return True
@@ -90,7 +90,7 @@
},
"alarm_arm_home": {
"name": "Arm home",
"description": "Arms the alarm in the home mode.",
"description": "Sets the alarm to: _armed, but someone is home_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
@@ -100,7 +100,7 @@
},
"alarm_arm_away": {
"name": "Arm away",
"description": "Arms the alarm in the away mode.",
"description": "Sets the alarm to: _armed, no one home_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
@@ -110,7 +110,7 @@
},
"alarm_arm_night": {
"name": "Arm night",
"description": "Arms the alarm in the night mode.",
"description": "Sets the alarm to: _armed for the night_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
@@ -120,7 +120,7 @@
},
"alarm_arm_vacation": {
"name": "Arm vacation",
"description": "Arms the alarm in the vacation mode.",
"description": "Sets the alarm to: _armed for vacation_.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
@@ -130,7 +130,7 @@
},
"alarm_trigger": {
"name": "Trigger",
"description": "Triggers the alarm manually.",
"description": "Trigger the alarm manually.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["adext", "alarmdecoder"],
"requirements": ["adext==0.4.4"]
"requirements": ["adext==0.4.3"]
}
+2 -3
View File
@@ -14,7 +14,7 @@ from homeassistant.components.notify import (
)
from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_ON
from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant
from homeassistant.exceptions import ServiceNotFound, ServiceValidationError
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (
async_track_point_in_time,
@@ -195,8 +195,7 @@ class AlertEntity(Entity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Async Acknowledge alert."""
if not self._can_ack:
raise ServiceValidationError("This alert cannot be acknowledged")
LOGGER.debug("Acknowledged Alert: %s", self._attr_name)
self._ack = True
self.async_write_ha_state()
@@ -1438,7 +1438,7 @@ class AlexaModeController(AlexaCapability):
# Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, ()):
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
return f"{fan.ATTR_PRESET_MODE}.{mode}"
# Humidifier mode
+6 -3
View File
@@ -719,7 +719,7 @@ class LockCapabilities(AlexaEntity):
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
class MediaPlayerCapabilities(AlexaEntity):
"""Class to represent MediaPlayer capabilities."""
@@ -757,7 +757,9 @@ class MediaPlayerCapabilities(AlexaEntity):
if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
inputs = AlexaInputController.get_valid_inputs(
self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, [])
self.entity.attributes.get(
media_player.const.ATTR_INPUT_SOURCE_LIST, []
)
)
if len(inputs) > 0:
yield AlexaInputController(self.entity)
@@ -774,7 +776,8 @@ class MediaPlayerCapabilities(AlexaEntity):
and domain != "denonavr"
):
inputs = AlexaEqualizerController.get_valid_inputs(
self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
or []
)
if len(inputs) > 0:
yield AlexaEqualizerController(self.entity)
+14 -12
View File
@@ -566,7 +566,7 @@ async def async_api_set_volume(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
await hass.services.async_call(
@@ -589,7 +589,7 @@ async def async_api_select_input(
# Attempt to map the ALL UPPERCASE payload name to a source.
# Strips trailing 1 to match single input devices.
source_list = entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or []
for source in source_list:
formatted_source = (
source.lower().replace("-", "").replace("_", "").replace(" ", "")
@@ -611,7 +611,7 @@ async def async_api_select_input(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_INPUT_SOURCE: media_input,
media_player.const.ATTR_INPUT_SOURCE: media_input,
}
await hass.services.async_call(
@@ -636,7 +636,7 @@ async def async_api_adjust_volume(
volume_delta = int(directive.payload["volume"])
entity = directive.entity
current_level = entity.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL]
current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL]
# read current state
try:
@@ -648,7 +648,7 @@ async def async_api_adjust_volume(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
await hass.services.async_call(
@@ -709,7 +709,7 @@ async def async_api_set_mute(
entity = directive.entity
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
}
await hass.services.async_call(
@@ -1708,13 +1708,15 @@ async def async_api_changechannel(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_CONTENT_ID: channel,
media_player.ATTR_MEDIA_CONTENT_TYPE: (media_player.MediaType.CHANNEL),
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
media_player.const.MEDIA_TYPE_CHANNEL
),
}
await hass.services.async_call(
entity.domain,
media_player.SERVICE_PLAY_MEDIA,
media_player.const.SERVICE_PLAY_MEDIA,
data,
blocking=False,
context=context,
@@ -1823,13 +1825,13 @@ async def async_api_set_eq_mode(
context: ha.Context,
) -> AlexaResponse:
"""Process a SetMode request for EqualizerController."""
mode: str = directive.payload["mode"]
mode = directive.payload["mode"]
entity = directive.entity
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
sound_mode_list = entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST)
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
if sound_mode_list and mode.lower() in sound_mode_list:
data[media_player.ATTR_SOUND_MODE] = mode.lower()
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
else:
msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}"
raise AlexaInvalidValueError(msg)
@@ -3,10 +3,10 @@
from __future__ import annotations
from asyncio import timeout
from collections.abc import Mapping
from http import HTTPStatus
import json
import logging
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast
from uuid import uuid4
@@ -260,10 +260,10 @@ async def async_enable_proactive_mode(
def extra_significant_check(
hass: HomeAssistant,
old_state: str,
old_attrs: Mapping[Any, Any],
old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
old_extra_arg: Any,
new_state: str,
new_attrs: Mapping[Any, Any],
new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
new_extra_arg: Any,
) -> bool:
"""Check if the serialized data has changed."""
@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"],
"quality_scale": "legacy",
"requirements": ["boto3==1.37.1"]
"requirements": ["boto3==1.34.131"]
}
@@ -239,8 +239,6 @@ SENSOR_DESCRIPTIONS = (
native_unit_of_measurement=DEGREE,
suggested_display_precision=0,
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
SensorEntityDescription(
key=TYPE_WINDGUSTMPH,
@@ -608,26 +608,21 @@ SENSOR_DESCRIPTIONS = (
key=TYPE_WINDDIR,
translation_key="wind_direction",
native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
),
SensorEntityDescription(
key=TYPE_WINDDIR_AVG10M,
translation_key="wind_direction_average_10m",
native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION,
),
SensorEntityDescription(
key=TYPE_WINDDIR_AVG2M,
translation_key="wind_direction_average_2m",
native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION,
),
SensorEntityDescription(
key=TYPE_WINDGUSTDIR,
translation_key="wind_gust_direction",
native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION,
),
SensorEntityDescription(
key=TYPE_WINDGUSTMPH,
@@ -8,7 +8,7 @@ from python_homeassistant_analytics import (
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
)
from python_homeassistant_analytics.models import Environment, IntegrationType
from python_homeassistant_analytics.models import IntegrationType
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
@@ -81,7 +81,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
)
try:
addons = await client.get_addons()
integrations = await client.get_integrations(Environment.NEXT)
integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
LOGGER.exception("Error connecting to Home Assistant analytics")
@@ -165,7 +165,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
)
try:
addons = await client.get_addons()
integrations = await client.get_integrations(Environment.NEXT)
integrations = await client.get_integrations()
custom_integrations = await client.get_custom_integrations()
except HomeassistantAnalyticsConnectionError:
LOGGER.exception("Error connecting to Home Assistant analytics")
@@ -3,12 +3,12 @@
"step": {
"user": {
"data": {
"tracked_addons": "Add-ons",
"tracked_addons": "Addons",
"tracked_integrations": "Integrations",
"tracked_custom_integrations": "Custom integrations"
},
"data_description": {
"tracked_addons": "Select the add-ons you want to track",
"tracked_addons": "Select the addons you want to track",
"tracked_integrations": "Select the integrations you want to track",
"tracked_custom_integrations": "Select the custom integrations you want to track"
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"iot_class": "local_polling",
"requirements": ["pydroid-ipcam==3.0.0"]
"requirements": ["pydroid-ipcam==2.0.0"]
}
@@ -73,7 +73,7 @@ class AndroidTVRemoteBaseEntity(Entity):
self._api.send_key_command(key_code, direction)
except ConnectionClosed as exc:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="connection_closed"
"Connection to Android TV device is closed"
) from exc
def _send_launch_app_command(self, app_link: str) -> None:
@@ -85,5 +85,5 @@ class AndroidTVRemoteBaseEntity(Entity):
self._api.send_launch_app_command(app_link)
except ConnectionClosed as exc:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="connection_closed"
"Connection to Android TV device is closed"
) from exc
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["androidtvremote2"],
"requirements": ["androidtvremote2==0.2.1"],
"requirements": ["androidtvremote2==0.1.2"],
"zeroconf": ["_androidtvremote2._tcp.local."]
}
@@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AndroidTVRemoteConfigEntry
from .const import CONF_APP_ICON, CONF_APP_NAME, DOMAIN
from .const import CONF_APP_ICON, CONF_APP_NAME
from .entity import AndroidTVRemoteBaseEntity
PARALLEL_UPDATES = 0
@@ -233,5 +233,5 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
await asyncio.sleep(delay_secs)
except ConnectionClosed as exc:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="connection_closed"
"Connection to Android TV device is closed"
) from exc
@@ -54,10 +54,5 @@
}
}
}
},
"exceptions": {
"connection_closed": {
"message": "Connection to the Android TV device is closed"
}
}
}
@@ -2,8 +2,6 @@
from __future__ import annotations
import logging
from anova_wifi import AnovaApi, InvalidLogin
import voluptuous as vol
@@ -13,10 +11,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AnovaConfigFlow(ConfigFlow, domain=DOMAIN):
class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
"""Sets up a config flow for Anova."""
VERSION = 1
@@ -39,8 +35,7 @@ class AnovaConfigFlow(ConfigFlow, domain=DOMAIN):
await api.authenticate()
except InvalidLogin:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
except Exception: # noqa: BLE001
errors["base"] = "unknown"
else:
return self.async_create_entry(
@@ -22,7 +22,6 @@ from . import AnthemavConfigEntry
from .const import ANTHEMAV_UPDATE_SIGNAL, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
VOLUME_STEP = 0.01
async def async_setup_entry(
@@ -61,7 +60,6 @@ class AnthemAVR(MediaPlayerEntity):
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.SELECT_SOURCE
)
_attr_volume_step = VOLUME_STEP
def __init__(
self,
@@ -10,7 +10,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"cannot_receive_deviceinfo": "Failed to retrieve MAC Address. Make sure the device is turned on"
"cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
@@ -2,8 +2,6 @@
from __future__ import annotations
from functools import partial
import anthropic
from homeassistant.config_entries import ConfigEntry
@@ -12,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .const import CONF_CHAT_MODEL, DOMAIN, LOGGER, RECOMMENDED_CHAT_MODEL
from .const import DOMAIN, LOGGER
PLATFORMS = (Platform.CONVERSATION,)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -22,13 +20,14 @@ type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
"""Set up Anthropic from a config entry."""
client = await hass.async_add_executor_job(
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
)
client = anthropic.AsyncAnthropic(api_key=entry.data[CONF_API_KEY])
try:
model_id = entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
LOGGER.debug("Anthropic model: %s", model.display_name)
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False
@@ -2,8 +2,6 @@
from __future__ import annotations
from collections.abc import Mapping
from functools import partial
import logging
from types import MappingProxyType
from typing import Any
@@ -35,12 +33,10 @@ from .const import (
CONF_PROMPT,
CONF_RECOMMENDED,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
DOMAIN,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
)
_LOGGER = logging.getLogger(__name__)
@@ -53,7 +49,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
RECOMMENDED_OPTIONS = {
CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
}
@@ -63,10 +59,13 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
client = await hass.async_add_executor_job(
partial(anthropic.AsyncAnthropic, api_key=data[CONF_API_KEY])
client = anthropic.AsyncAnthropic(api_key=data[CONF_API_KEY])
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
await client.models.list(timeout=10.0)
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -131,36 +130,25 @@ class AnthropicOptionsFlow(OptionsFlow):
) -> ConfigFlowResult:
"""Manage the options."""
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
errors: dict[str, str] = {}
if user_input is not None:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if not user_input.get(CONF_LLM_HASS_API):
user_input.pop(CONF_LLM_HASS_API, None)
if user_input.get(
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
errors[CONF_THINKING_BUDGET] = "thinking_budget_too_large"
if user_input[CONF_LLM_HASS_API] == "none":
user_input.pop(CONF_LLM_HASS_API)
return self.async_create_entry(title="", data=user_input)
if not errors:
return self.async_create_entry(title="", data=user_input)
else:
# Re-render the options again, now with the recommended options shown/hidden
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
# Re-render the options again, now with the recommended options shown/hidden
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
options = {
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
}
options = {
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
}
suggested_values = options.copy()
if not suggested_values.get(CONF_PROMPT):
suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT
if (
suggested_llm_apis := suggested_values.get(CONF_LLM_HASS_API)
) and isinstance(suggested_llm_apis, str):
suggested_values[CONF_LLM_HASS_API] = [suggested_llm_apis]
schema = self.add_suggested_values_to_schema(
vol.Schema(anthropic_config_option_schema(self.hass, options)),
@@ -170,28 +158,33 @@ class AnthropicOptionsFlow(OptionsFlow):
return self.async_show_form(
step_id="init",
data_schema=schema,
errors=errors or None,
)
def anthropic_config_option_schema(
hass: HomeAssistant,
options: Mapping[str, Any],
options: dict[str, Any] | MappingProxyType[str, Any],
) -> dict:
"""Return a schema for Anthropic completion options."""
hass_apis: list[SelectOptionDict] = [
SelectOptionDict(
label="No control",
value="none",
)
]
hass_apis.extend(
SelectOptionDict(
label=api.name,
value=api.id,
)
for api in llm.async_get_apis(hass)
]
)
schema = {
vol.Optional(CONF_PROMPT): TemplateSelector(),
vol.Optional(
CONF_LLM_HASS_API,
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
vol.Optional(CONF_LLM_HASS_API, default="none"): SelectSelector(
SelectSelectorConfig(options=hass_apis)
),
vol.Required(
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
): bool,
@@ -214,10 +207,6 @@ def anthropic_config_option_schema(
CONF_TEMPERATURE,
default=RECOMMENDED_TEMPERATURE,
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
vol.Optional(
CONF_THINKING_BUDGET,
default=RECOMMENDED_THINKING_BUDGET,
): int,
}
)
return schema
@@ -13,8 +13,3 @@ CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 1024
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0
CONF_THINKING_BUDGET = "thinking_budget"
RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024
THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-3-7-sonnet-latest"]
+115 -289
View File
@@ -1,39 +1,20 @@
"""Conversation support for Anthropic."""
from collections.abc import AsyncGenerator, Callable, Iterable
from collections.abc import Callable
import json
from typing import Any, Literal, cast
import anthropic
from anthropic import AsyncStream
from anthropic._types import NOT_GIVEN
from anthropic.types import (
InputJSONDelta,
MessageDeltaUsage,
Message,
MessageParam,
MessageStreamEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
RawMessageStopEvent,
RedactedThinkingBlock,
RedactedThinkingBlockParam,
SignatureDelta,
TextBlock,
TextBlockParam,
TextDelta,
ThinkingBlock,
ThinkingBlockParam,
ThinkingConfigDisabledParam,
ThinkingConfigEnabledParam,
ThinkingDelta,
ToolParam,
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
)
from voluptuous_openapi import convert
@@ -42,7 +23,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, intent, llm
from homeassistant.helpers import chat_session, device_registry as dr, intent, llm
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AnthropicConfigEntry
@@ -51,15 +32,11 @@ from .const import (
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_TEMPERATURE,
CONF_THINKING_BUDGET,
DOMAIN,
LOGGER,
MIN_THINKING_BUDGET,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_THINKING_BUDGET,
THINKING_MODELS,
)
# Max number of back and forth with the LLM to generate a response
@@ -87,236 +64,64 @@ def _format_tool(
)
def _convert_content(
chat_content: Iterable[conversation.Content],
) -> list[MessageParam]:
"""Transform HA chat_log content into Anthropic API format."""
messages: list[MessageParam] = []
def _message_convert(
message: Message,
) -> MessageParam:
"""Convert from class to TypedDict."""
param_content: list[TextBlockParam | ToolUseBlockParam] = []
for content in chat_content:
if isinstance(content, conversation.ToolResultContent):
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
)
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=[tool_result_block],
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
tool_result_block,
]
else:
messages[-1]["content"].append(tool_result_block) # type: ignore[attr-defined]
elif isinstance(content, conversation.UserContent):
# Combine consequent user messages
if not messages or messages[-1]["role"] != "user":
messages.append(
MessageParam(
role="user",
content=content.content,
)
)
elif isinstance(messages[-1]["content"], str):
messages[-1]["content"] = [
TextBlockParam(type="text", text=messages[-1]["content"]),
TextBlockParam(type="text", text=content.content),
]
else:
messages[-1]["content"].append( # type: ignore[attr-defined]
TextBlockParam(type="text", text=content.content)
)
elif isinstance(content, conversation.AssistantContent):
# Combine consequent assistant messages
if not messages or messages[-1]["role"] != "assistant":
messages.append(
MessageParam(
role="assistant",
content=[],
)
)
if content.content:
messages[-1]["content"].append( # type: ignore[union-attr]
TextBlockParam(type="text", text=content.content)
)
if content.tool_calls:
messages[-1]["content"].extend( # type: ignore[union-attr]
[
ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
name=tool_call.tool_name,
input=tool_call.tool_args,
)
for tool_call in content.tool_calls
]
)
else:
# Note: We don't pass SystemContent here as its passed to the API as the prompt
raise TypeError(f"Unexpected content type: {type(content)}")
return messages
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
result: AsyncStream[MessageStreamEvent],
messages: list[MessageParam],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
"""Transform the response stream into HA format.
A typical stream of responses might look something like the following:
- RawMessageStartEvent with no content
- RawContentBlockStartEvent with an empty ThinkingBlock (if extended thinking is enabled)
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- RawContentBlockDeltaEvent with a ThinkingDelta
- ...
- RawContentBlockDeltaEvent with a SignatureDelta
- RawContentBlockStopEvent
- RawContentBlockStartEvent with a RedactedThinkingBlock (occasionally)
- RawContentBlockStopEvent (RedactedThinkingBlock does not have a delta)
- RawContentBlockStartEvent with an empty TextBlock
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- RawContentBlockDeltaEvent with a TextDelta
- ...
- RawContentBlockStopEvent
- RawContentBlockStartEvent with ToolUseBlock specifying the function name
- RawContentBlockDeltaEvent with a InputJSONDelta
- RawContentBlockDeltaEvent with a InputJSONDelta
- ...
- RawContentBlockStopEvent
- RawMessageDeltaEvent with a stop_reason='tool_use'
- RawMessageStopEvent(type='message_stop')
Each message could contain multiple blocks of the same type.
"""
if result is None:
raise TypeError("Expected a stream of messages")
current_message: MessageParam | None = None
current_block: (
TextBlockParam
| ToolUseBlockParam
| ThinkingBlockParam
| RedactedThinkingBlockParam
| None
) = None
current_tool_args: str
input_usage: Usage | None = None
async for response in result:
LOGGER.debug("Received response: %s", response)
if isinstance(response, RawMessageStartEvent):
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
current_message = MessageParam(role=response.message.role, content=[])
input_usage = response.message.usage
elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock):
current_block = ToolUseBlockParam(
for message_content in message.content:
if isinstance(message_content, TextBlock):
param_content.append(TextBlockParam(type="text", text=message_content.text))
elif isinstance(message_content, ToolUseBlock):
param_content.append(
ToolUseBlockParam(
type="tool_use",
id=response.content_block.id,
name=response.content_block.name,
input="",
id=message_content.id,
name=message_content.name,
input=message_content.input,
)
current_tool_args = ""
elif isinstance(response.content_block, TextBlock):
current_block = TextBlockParam(
type="text", text=response.content_block.text
)
yield {"role": "assistant"}
if response.content_block.text:
yield {"content": response.content_block.text}
elif isinstance(response.content_block, ThinkingBlock):
current_block = ThinkingBlockParam(
type="thinking",
thinking=response.content_block.thinking,
signature=response.content_block.signature,
)
elif isinstance(response.content_block, RedactedThinkingBlock):
current_block = RedactedThinkingBlockParam(
type="redacted_thinking", data=response.content_block.data
)
LOGGER.debug(
"Some of Claudes internal reasoning has been automatically "
"encrypted for safety reasons. This doesnt affect the quality of "
"responses"
)
elif isinstance(response, RawContentBlockDeltaEvent):
if current_block is None:
raise ValueError("Unexpected delta without a block")
if isinstance(response.delta, InputJSONDelta):
current_tool_args += response.delta.partial_json
elif isinstance(response.delta, TextDelta):
text_block = cast(TextBlockParam, current_block)
text_block["text"] += response.delta.text
yield {"content": response.delta.text}
elif isinstance(response.delta, ThinkingDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["thinking"] += response.delta.thinking
elif isinstance(response.delta, SignatureDelta):
thinking_block = cast(ThinkingBlockParam, current_block)
thinking_block["signature"] += response.delta.signature
elif isinstance(response, RawContentBlockStopEvent):
if current_block is None:
raise ValueError("Unexpected stop event without a current block")
if current_block["type"] == "tool_use":
# tool block
tool_args = json.loads(current_tool_args) if current_tool_args else {}
current_block["input"] = tool_args
yield {
"tool_calls": [
llm.ToolInput(
id=current_block["id"],
tool_name=current_block["name"],
tool_args=tool_args,
)
]
}
elif current_block["type"] == "thinking":
# thinking block
LOGGER.debug("Thinking: %s", current_block["thinking"])
)
if current_message is None:
raise ValueError("Unexpected stop event without a current message")
current_message["content"].append(current_block) # type: ignore[union-attr]
current_block = None
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
elif isinstance(response, RawMessageStopEvent):
if current_message is not None:
messages.append(current_message)
current_message = None
return MessageParam(role=message.role, content=param_content)
def _create_token_stats(
input_usage: Usage | None, response_usage: MessageDeltaUsage
) -> dict[str, Any]:
"""Create token stats for conversation agent tracing."""
input_tokens = 0
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
"input_tokens": input_tokens,
"cached_input_tokens": cached_input_tokens,
"output_tokens": output_tokens,
}
}
def _convert_content(chat_content: conversation.Content) -> MessageParam:
"""Create tool response content."""
if isinstance(chat_content, conversation.ToolResultContent):
return MessageParam(
role="user",
content=[
ToolResultBlockParam(
type="tool_result",
tool_use_id=chat_content.tool_call_id,
content=json.dumps(chat_content.tool_result),
)
],
)
if isinstance(chat_content, conversation.AssistantContent):
return MessageParam(
role="assistant",
content=[
TextBlockParam(type="text", text=chat_content.content or ""),
*[
ToolUseBlockParam(
type="tool_use",
id=tool_call.id,
name=tool_call.tool_name,
input=json.dumps(tool_call.tool_args),
)
for tool_call in chat_content.tool_calls or ()
],
],
)
if isinstance(chat_content, conversation.UserContent):
return MessageParam(
role="user",
content=chat_content.content,
)
# Note: We don't pass SystemContent here as its passed to the API as the prompt
raise ValueError(f"Unexpected content type: {type(chat_content)}")
class AnthropicConversationEntity(
@@ -354,6 +159,18 @@ class AnthropicConversationEntity(
self.entry.add_update_listener(self._async_entry_update_listener)
)
async def async_process(
self, user_input: conversation.ConversationInput
) -> conversation.ConversationResult:
"""Process a sentence."""
with (
chat_session.async_get_chat_session(
self.hass, user_input.conversation_id
) as session,
conversation.async_get_chat_log(self.hass, session, user_input) as chat_log,
):
return await self._async_handle_message(user_input, chat_log)
async def _async_handle_message(
self,
user_input: conversation.ConversationInput,
@@ -382,54 +199,65 @@ class AnthropicConversationEntity(
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise TypeError("First message must be a system message")
messages = _convert_content(chat_log.content[1:])
messages = [_convert_content(content) for content in chat_log.content[1:]]
client = self.entry.runtime_data
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
model_args = {
"model": model,
"messages": messages,
"tools": tools or NOT_GIVEN,
"max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
"system": system.content,
"stream": True,
}
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
)
else:
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
model_args["temperature"] = options.get(
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
)
try:
stream = await client.messages.create(**model_args)
response = await client.messages.create(
model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
messages=messages,
tools=tools or NOT_GIVEN,
max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
system=system.content,
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
)
except anthropic.AnthropicError as err:
raise HomeAssistantError(
f"Sorry, I had a problem talking to Anthropic: {err}"
) from err
messages.extend(
_convert_content(
[
content
async for content in chat_log.async_add_delta_content_stream(
user_input.agent_id,
_transform_stream(chat_log, stream, messages),
)
if not isinstance(content, conversation.AssistantContent)
]
)
)
LOGGER.debug("Response %s", response)
if not chat_log.unresponded_tool_results:
messages.append(_message_convert(response))
text = "".join(
[
content.text
for content in response.content
if isinstance(content, TextBlock)
]
)
tool_inputs = [
llm.ToolInput(
id=tool_call.id,
tool_name=tool_call.name,
tool_args=cast(dict[str, Any], tool_call.input),
)
for tool_call in response.content
if isinstance(tool_call, ToolUseBlock)
]
tool_results = [
ToolResultBlockParam(
type="tool_result",
tool_use_id=tool_response.tool_call_id,
content=json.dumps(tool_response.tool_result),
)
async for tool_response in chat_log.async_add_assistant_content(
conversation.AssistantContent(
agent_id=user_input.agent_id,
content=text,
tool_calls=tool_inputs or None,
)
)
]
if tool_results:
messages.append(MessageParam(role="user", content=tool_results))
if not tool_inputs:
break
response_content = chat_log.content[-1]
@@ -438,9 +266,7 @@ class AnthropicConversationEntity(
intent_response = intent.IntentResponse(language=user_input.language)
intent_response.async_set_speech(response_content.content or "")
return conversation.ConversationResult(
response=intent_response,
conversation_id=chat_log.conversation_id,
continue_conversation=chat_log.continue_conversation,
response=intent_response, conversation_id=chat_log.conversation_id
)
async def _async_entry_update_listener(
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.47.2"]
"requirements": ["anthropic==0.44.0"]
}
@@ -23,17 +23,12 @@
"max_tokens": "Maximum tokens to return in response",
"temperature": "Temperature",
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
"recommended": "Recommended model settings",
"thinking_budget_tokens": "Thinking budget"
"recommended": "Recommended model settings"
},
"data_description": {
"prompt": "Instruct how the LLM should respond. This can be a template.",
"thinking_budget_tokens": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking."
"prompt": "Instruct how the LLM should respond. This can be a template."
}
}
},
"error": {
"thinking_budget_too_large": "Maximum tokens must be greater than the thinking budget."
}
}
}
@@ -53,8 +53,10 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
"""Initialize the APCUPSd binary device."""
super().__init__(coordinator, context=description.key.upper())
# Set up unique id and device info if serial number is available.
if (serial_no := coordinator.data.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}"
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
self._attr_device_info = coordinator.device_info
@property
@@ -85,16 +85,11 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
self._host = host
self._port = port
@property
def unique_device_id(self) -> str:
"""Return a unique ID of the device, which is the serial number (if available) or the config entry ID."""
return self.data.serial_no or self.config_entry.entry_id
@property
def device_info(self) -> DeviceInfo:
"""Return the DeviceInfo of this APC UPS, if serial number is available."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_device_id)},
identifiers={(DOMAIN, self.data.serial_no or self.config_entry.entry_id)},
model=self.data.model,
manufacturer="APC",
name=self.data.name or "APC UPS",
@@ -113,7 +108,4 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
data = await aioapcaccess.request_status(self._host, self._port)
return APCUPSdData(data)
except (OSError, asyncio.IncompleteReadError) as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from error
raise UpdateFailed(error) from error
+4 -1
View File
@@ -458,8 +458,11 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
"""Initialize the sensor."""
super().__init__(coordinator=coordinator, context=description.key.upper())
# Set up unique id and device info if serial number is available.
if (serial_no := coordinator.data.serial_no) is not None:
self._attr_unique_id = f"{serial_no}_{description.key}"
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
self._attr_device_info = coordinator.device_info
# Initial update of attributes.
@@ -57,7 +57,7 @@
"name": "Status date"
},
"dip_switch_settings": {
"name": "DIP switch settings"
"name": "Dip switch settings"
},
"low_battery_signal": {
"name": "Low battery signal"
@@ -93,7 +93,7 @@
"name": "Internal temperature"
},
"last_self_test": {
"name": "Last self-test"
"name": "Last self test"
},
"last_transfer": {
"name": "Last transfer"
@@ -177,7 +177,7 @@
"name": "Restore requirement"
},
"self_test_result": {
"name": "Self-test result"
"name": "Self test result"
},
"sensitivity": {
"name": "Sensitivity"
@@ -195,7 +195,7 @@
"name": "Status"
},
"self_test_interval": {
"name": "Self-test interval"
"name": "Self test interval"
},
"time_left": {
"name": "Time left"
@@ -219,10 +219,5 @@
"name": "Transfer to battery"
}
}
},
"exceptions": {
"cannot_connect": {
"message": "Cannot connect to APC UPS Daemon."
}
}
}
@@ -1 +0,0 @@
"""Virtual integration: Apollo Automation."""
@@ -1,6 +0,0 @@
{
"domain": "apollo_automation",
"name": "Apollo Automation",
"integration_type": "virtual",
"supported_by": "esphome"
}
@@ -233,6 +233,7 @@ class AppleTVManager(DeviceListener):
pass
except Exception:
_LOGGER.exception("Failed to connect")
await self.disconnect()
async def _connect_loop(self) -> None:
"""Connect loop background task function."""
@@ -20,7 +20,6 @@ import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_ZEROCONF,
ConfigEntry,
ConfigFlow,
@@ -382,9 +381,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_IDENTIFIERS: list(combined_identifiers),
},
)
# Don't reload ignored entries or in the middle of reauth,
# e.g. if the user is entering a new PIN
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
if entry.source != SOURCE_IGNORE:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
if not allow_exist:
raise DeviceAlreadyConfigured
@@ -120,7 +120,6 @@ class AppleTvMediaPlayer(
"""Initialize the Apple TV media player."""
super().__init__(name, identifier, manager)
self._playing: Playing | None = None
self._playing_last_updated: datetime | None = None
self._app_list: dict[str, str] = {}
@callback
@@ -210,7 +209,6 @@ class AppleTvMediaPlayer(
This is a callback function from pyatv.interface.PushListener.
"""
self._playing = playstatus
self._playing_last_updated = dt_util.utcnow()
self.async_write_ha_state()
@callback
@@ -318,7 +316,7 @@ class AppleTvMediaPlayer(
def media_position_updated_at(self) -> datetime | None:
"""Last valid time of media position."""
if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}:
return self._playing_last_updated
return dt_util.utcnow()
return None
async def async_play_media(
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.8.1"]
"requirements": ["pyaprilaire==0.7.7"]
}
@@ -43,7 +43,6 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
config_entry: ApSystemsConfigEntry
device_version: str
battery_system: bool
def __init__(
self,
@@ -69,7 +68,6 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
self.api.max_power = device_info.maxPower
self.api.min_power = device_info.minPower
self.device_version = device_info.devVer
self.battery_system = device_info.isBatterySystem
async def _async_update_data(self) -> ApSystemsSensorData:
try:
+1 -11
View File
@@ -19,20 +19,10 @@ class ApSystemsEntity(Entity):
data: ApSystemsData,
) -> None:
"""Initialize the APsystems entity."""
# Handle device version safely
sw_version = None
if data.coordinator.device_version:
version_parts = data.coordinator.device_version.split(" ")
if len(version_parts) > 1:
sw_version = version_parts[1]
else:
sw_version = version_parts[0]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, data.device_id)},
manufacturer="APsystems",
model="EZ1-M",
serial_number=data.device_id,
sw_version=sw_version,
sw_version=data.coordinator.device_version.split(" ")[1],
)
@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["APsystemsEZ1"],
"requirements": ["apsystems-ez1==2.6.0"]
"requirements": ["apsystems-ez1==2.4.0"]
}
@@ -21,7 +21,7 @@
"entity": {
"binary_sensor": {
"off_grid_status": {
"name": "Off-grid status"
"name": "Off grid status"
},
"dc_1_short_circuit_error_status": {
"name": "DC 1 short circuit error status"
@@ -36,8 +36,6 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
super().__init__(data)
self._api = data.coordinator.api
self._attr_unique_id = f"{data.device_id}_inverter_status"
if data.coordinator.battery_system:
self._attr_available = False
async def async_update(self) -> None:
"""Update switch status and availability."""

Some files were not shown because too many files have changed in this diff Show More