This commit is contained in:
Franck Nijhof 2024-04-03 20:38:11 +02:00 committed by GitHub
commit b61397656c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10441 changed files with 140954 additions and 68334 deletions

View File

@ -202,6 +202,7 @@ omit =
homeassistant/components/control4/__init__.py homeassistant/components/control4/__init__.py
homeassistant/components/control4/director_utils.py homeassistant/components/control4/director_utils.py
homeassistant/components/control4/light.py homeassistant/components/control4/light.py
homeassistant/components/control4/media_player.py
homeassistant/components/coolmaster/coordinator.py homeassistant/components/coolmaster/coordinator.py
homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/crownstone/__init__.py homeassistant/components/crownstone/__init__.py
@ -250,7 +251,7 @@ omit =
homeassistant/components/dormakaba_dkey/lock.py homeassistant/components/dormakaba_dkey/lock.py
homeassistant/components/dormakaba_dkey/sensor.py homeassistant/components/dormakaba_dkey/sensor.py
homeassistant/components/dovado/* homeassistant/components/dovado/*
homeassistant/components/downloader/* homeassistant/components/downloader/__init__.py
homeassistant/components/dsmr_reader/__init__.py homeassistant/components/dsmr_reader/__init__.py
homeassistant/components/dsmr_reader/definitions.py homeassistant/components/dsmr_reader/definitions.py
homeassistant/components/dsmr_reader/sensor.py homeassistant/components/dsmr_reader/sensor.py
@ -461,6 +462,10 @@ omit =
homeassistant/components/frontier_silicon/browse_media.py homeassistant/components/frontier_silicon/browse_media.py
homeassistant/components/frontier_silicon/media_player.py homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py homeassistant/components/futurenow/light.py
homeassistant/components/fyta/__init__.py
homeassistant/components/fyta/coordinator.py
homeassistant/components/fyta/entity.py
homeassistant/components/fyta/sensor.py
homeassistant/components/garadget/cover.py homeassistant/components/garadget/cover.py
homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/__init__.py
homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/binary_sensor.py
@ -545,7 +550,6 @@ omit =
homeassistant/components/homematic/notify.py homeassistant/components/homematic/notify.py
homeassistant/components/homematic/sensor.py homeassistant/components/homematic/sensor.py
homeassistant/components/homematic/switch.py homeassistant/components/homematic/switch.py
homeassistant/components/homeworks/*
homeassistant/components/horizon/media_player.py homeassistant/components/horizon/media_player.py
homeassistant/components/hp_ilo/sensor.py homeassistant/components/hp_ilo/sensor.py
homeassistant/components/huawei_lte/__init__.py homeassistant/components/huawei_lte/__init__.py
@ -744,7 +748,6 @@ omit =
homeassistant/components/lyric/climate.py homeassistant/components/lyric/climate.py
homeassistant/components/lyric/sensor.py homeassistant/components/lyric/sensor.py
homeassistant/components/mailgun/notify.py homeassistant/components/mailgun/notify.py
homeassistant/components/map/*
homeassistant/components/mastodon/notify.py homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/__init__.py homeassistant/components/matrix/__init__.py
homeassistant/components/matrix/notify.py homeassistant/components/matrix/notify.py
@ -773,9 +776,11 @@ omit =
homeassistant/components/microbees/__init__.py homeassistant/components/microbees/__init__.py
homeassistant/components/microbees/api.py homeassistant/components/microbees/api.py
homeassistant/components/microbees/application_credentials.py homeassistant/components/microbees/application_credentials.py
homeassistant/components/microbees/binary_sensor.py
homeassistant/components/microbees/button.py homeassistant/components/microbees/button.py
homeassistant/components/microbees/const.py homeassistant/components/microbees/const.py
homeassistant/components/microbees/coordinator.py homeassistant/components/microbees/coordinator.py
homeassistant/components/microbees/cover.py
homeassistant/components/microbees/entity.py homeassistant/components/microbees/entity.py
homeassistant/components/microbees/light.py homeassistant/components/microbees/light.py
homeassistant/components/microbees/sensor.py homeassistant/components/microbees/sensor.py
@ -801,6 +806,11 @@ omit =
homeassistant/components/motion_blinds/cover.py homeassistant/components/motion_blinds/cover.py
homeassistant/components/motion_blinds/entity.py homeassistant/components/motion_blinds/entity.py
homeassistant/components/motion_blinds/sensor.py homeassistant/components/motion_blinds/sensor.py
homeassistant/components/motionblinds_ble/__init__.py
homeassistant/components/motionblinds_ble/button.py
homeassistant/components/motionblinds_ble/cover.py
homeassistant/components/motionblinds_ble/entity.py
homeassistant/components/motionblinds_ble/select.py
homeassistant/components/motionmount/__init__.py homeassistant/components/motionmount/__init__.py
homeassistant/components/motionmount/binary_sensor.py homeassistant/components/motionmount/binary_sensor.py
homeassistant/components/motionmount/entity.py homeassistant/components/motionmount/entity.py
@ -888,6 +898,7 @@ omit =
homeassistant/components/notify_events/notify.py homeassistant/components/notify_events/notify.py
homeassistant/components/notion/__init__.py homeassistant/components/notion/__init__.py
homeassistant/components/notion/binary_sensor.py homeassistant/components/notion/binary_sensor.py
homeassistant/components/notion/coordinator.py
homeassistant/components/notion/sensor.py homeassistant/components/notion/sensor.py
homeassistant/components/notion/util.py homeassistant/components/notion/util.py
homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nsw_fuel_station/sensor.py
@ -923,7 +934,6 @@ omit =
homeassistant/components/onvif/sensor.py homeassistant/components/onvif/sensor.py
homeassistant/components/onvif/util.py homeassistant/components/onvif/util.py
homeassistant/components/open_meteo/weather.py homeassistant/components/open_meteo/weather.py
homeassistant/components/opencv/*
homeassistant/components/openevse/sensor.py homeassistant/components/openevse/sensor.py
homeassistant/components/openexchangerates/__init__.py homeassistant/components/openexchangerates/__init__.py
homeassistant/components/openexchangerates/coordinator.py homeassistant/components/openexchangerates/coordinator.py
@ -946,7 +956,9 @@ omit =
homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/binary_sensor.py
homeassistant/components/openuv/coordinator.py homeassistant/components/openuv/coordinator.py
homeassistant/components/openuv/sensor.py homeassistant/components/openuv/sensor.py
homeassistant/components/openweathermap/__init__.py
homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/sensor.py
homeassistant/components/openweathermap/weather.py
homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/openweathermap/weather_update_coordinator.py
homeassistant/components/opnsense/__init__.py homeassistant/components/opnsense/__init__.py
homeassistant/components/opower/__init__.py homeassistant/components/opower/__init__.py
@ -988,7 +1000,9 @@ omit =
homeassistant/components/pandora/media_player.py homeassistant/components/pandora/media_player.py
homeassistant/components/pencom/switch.py homeassistant/components/pencom/switch.py
homeassistant/components/permobil/__init__.py homeassistant/components/permobil/__init__.py
homeassistant/components/permobil/binary_sensor.py
homeassistant/components/permobil/coordinator.py homeassistant/components/permobil/coordinator.py
homeassistant/components/permobil/entity.py
homeassistant/components/permobil/sensor.py homeassistant/components/permobil/sensor.py
homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/__init__.py
homeassistant/components/philips_js/light.py homeassistant/components/philips_js/light.py
@ -1055,6 +1069,7 @@ omit =
homeassistant/components/rabbitair/fan.py homeassistant/components/rabbitair/fan.py
homeassistant/components/rachio/__init__.py homeassistant/components/rachio/__init__.py
homeassistant/components/rachio/binary_sensor.py homeassistant/components/rachio/binary_sensor.py
homeassistant/components/rachio/coordinator.py
homeassistant/components/rachio/device.py homeassistant/components/rachio/device.py
homeassistant/components/rachio/entity.py homeassistant/components/rachio/entity.py
homeassistant/components/rachio/switch.py homeassistant/components/rachio/switch.py
@ -1127,6 +1142,7 @@ omit =
homeassistant/components/rocketchat/notify.py homeassistant/components/rocketchat/notify.py
homeassistant/components/romy/__init__.py homeassistant/components/romy/__init__.py
homeassistant/components/romy/coordinator.py homeassistant/components/romy/coordinator.py
homeassistant/components/romy/entity.py
homeassistant/components/romy/vacuum.py homeassistant/components/romy/vacuum.py
homeassistant/components/roomba/__init__.py homeassistant/components/roomba/__init__.py
homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/binary_sensor.py
@ -1141,7 +1157,6 @@ omit =
homeassistant/components/roon/media_player.py homeassistant/components/roon/media_player.py
homeassistant/components/roon/server.py homeassistant/components/roon/server.py
homeassistant/components/route53/* homeassistant/components/route53/*
homeassistant/components/rova/sensor.py
homeassistant/components/rpi_camera/* homeassistant/components/rpi_camera/*
homeassistant/components/rtorrent/sensor.py homeassistant/components/rtorrent/sensor.py
homeassistant/components/ruuvi_gateway/__init__.py homeassistant/components/ruuvi_gateway/__init__.py
@ -1179,7 +1194,6 @@ omit =
homeassistant/components/serial_pm/sensor.py homeassistant/components/serial_pm/sensor.py
homeassistant/components/sesame/lock.py homeassistant/components/sesame/lock.py
homeassistant/components/seven_segments/image_processing.py homeassistant/components/seven_segments/image_processing.py
homeassistant/components/seventeentrack/sensor.py
homeassistant/components/shodan/sensor.py homeassistant/components/shodan/sensor.py
homeassistant/components/sia/__init__.py homeassistant/components/sia/__init__.py
homeassistant/components/sia/alarm_control_panel.py homeassistant/components/sia/alarm_control_panel.py
@ -1281,6 +1295,7 @@ omit =
homeassistant/components/starlink/device_tracker.py homeassistant/components/starlink/device_tracker.py
homeassistant/components/starlink/sensor.py homeassistant/components/starlink/sensor.py
homeassistant/components/starlink/switch.py homeassistant/components/starlink/switch.py
homeassistant/components/starlink/time.py
homeassistant/components/starline/__init__.py homeassistant/components/starline/__init__.py
homeassistant/components/starline/account.py homeassistant/components/starline/account.py
homeassistant/components/starline/binary_sensor.py homeassistant/components/starline/binary_sensor.py
@ -1425,6 +1440,7 @@ omit =
homeassistant/components/tolo/number.py homeassistant/components/tolo/number.py
homeassistant/components/tolo/select.py homeassistant/components/tolo/select.py
homeassistant/components/tolo/sensor.py homeassistant/components/tolo/sensor.py
homeassistant/components/tolo/switch.py
homeassistant/components/toon/__init__.py homeassistant/components/toon/__init__.py
homeassistant/components/toon/binary_sensor.py homeassistant/components/toon/binary_sensor.py
homeassistant/components/toon/climate.py homeassistant/components/toon/climate.py
@ -1594,7 +1610,6 @@ omit =
homeassistant/components/weatherflow_cloud/const.py homeassistant/components/weatherflow_cloud/const.py
homeassistant/components/weatherflow_cloud/coordinator.py homeassistant/components/weatherflow_cloud/coordinator.py
homeassistant/components/weatherflow_cloud/weather.py homeassistant/components/weatherflow_cloud/weather.py
homeassistant/components/webmin/sensor.py
homeassistant/components/wiffi/__init__.py homeassistant/components/wiffi/__init__.py
homeassistant/components/wiffi/binary_sensor.py homeassistant/components/wiffi/binary_sensor.py
homeassistant/components/wiffi/sensor.py homeassistant/components/wiffi/sensor.py
@ -1677,6 +1692,7 @@ omit =
homeassistant/components/yolink/services.py homeassistant/components/yolink/services.py
homeassistant/components/yolink/siren.py homeassistant/components/yolink/siren.py
homeassistant/components/yolink/switch.py homeassistant/components/yolink/switch.py
homeassistant/components/yolink/valve.py
homeassistant/components/youless/__init__.py homeassistant/components/youless/__init__.py
homeassistant/components/youless/sensor.py homeassistant/components/youless/sensor.py
homeassistant/components/zabbix/* homeassistant/components/zabbix/*

View File

@ -21,6 +21,7 @@
], ],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": { "settings": {
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.pythonPath": "/usr/local/bin/python", "python.pythonPath": "/usr/local/bin/python",
"python.testing.pytestArgs": ["--no-cov"], "python.testing.pytestArgs": ["--no-cov"],
"editor.formatOnPaste": false, "editor.formatOnPaste": false,

14
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,14 @@
# Black
4de97abc3aa83188666336ce0a015a5bab75bc8f
# Switch formatting from black to ruff-format (#102893)
706add4a57120a93d7b7fe40e722b00d634c76c2
# Prettify json (component test fixtures) (#68892)
053c4428a933c3c04c22642f93c93fccba3e8bfd
# Prettify json (tests) (#68888)
496d90bf00429d9d924caeb0155edc0bf54e86b9
# Bump ruff to 0.3.4 (#112690)
6bb4e7d62c60389608acf4a7d7dacd8f029307dd

View File

@ -12,6 +12,8 @@ env:
BUILD_TYPE: core BUILD_TYPE: core
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
PIP_TIMEOUT: 60 PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
jobs: jobs:
init: init:
@ -25,12 +27,12 @@ jobs:
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -49,41 +51,29 @@ jobs:
with: with:
ignore-dev: true ignore-dev: true
build_python: - name: Fail if translations files are checked in
name: Build PyPi package run: |
environment: ${{ needs.init.outputs.channel }} if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then
needs: ["init", "build_base"] echo "Translations files are checked in, please remove the following files:"
runs-on: ubuntu-latest find homeassistant/components/*/translations -type f
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' exit 1
steps: fi
- name: Checkout the repository
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download Translations - name: Download Translations
run: python3 -m script.translations download run: python3 -m script.translations download
env: env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build package - name: Archive translations
shell: bash shell: bash
run: | run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install twine build
python -m build
- name: Upload package - name: Upload translations
shell: bash uses: actions/upload-artifact@v4.3.1
run: | with:
export TWINE_USERNAME="__token__" name: translations
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" path: translations.tar.gz
if-no-files-found: error
twine upload dist/* --skip-existing
build_base: build_base:
name: Build ${{ matrix.arch }} base core image name: Build ${{ matrix.arch }} base core image
@ -95,15 +85,16 @@ jobs:
packages: write packages: write
id-token: write id-token: write
strategy: strategy:
fail-fast: false
matrix: matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v3.1.2 uses: dawidd6/action-download-artifact@v3.1.4
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend repo: home-assistant/frontend
@ -114,7 +105,7 @@ jobs:
- name: Download nightly wheels of intents - name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v3.1.2 uses: dawidd6/action-download-artifact@v3.1.4
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package repo: home-assistant/intents-package
@ -125,17 +116,20 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Adjust nightly version - name: Adjust nightly version
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
shell: bash shell: bash
env:
UV_PRERELEASE: allow
run: | run: |
python3 -m pip install packaging tomli python3 -m pip install "$(grep '^uv' < requirements_test.txt)"
python3 -m pip install . uv pip install packaging tomli
version="$(python3 script/version_bump.py nightly)" uv pip install .
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}"
@ -147,7 +141,7 @@ jobs:
sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \ sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \
homeassistant/package_constraints.txt homeassistant/package_constraints.txt
python -m script.gen_requirements_all sed -i "s|home-assistant-frontend==.*||" requirements_all.txt
fi fi
if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then
@ -165,7 +159,7 @@ jobs:
sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \ sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \
homeassistant/package_constraints.txt homeassistant/package_constraints.txt
python -m script.gen_requirements_all sed -i "s|home-assistant-intents==.*||" requirements_all.txt
fi fi
- name: Adjustments for armhf - name: Adjustments for armhf
@ -189,10 +183,15 @@ jobs:
# are not available. # are not available.
sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt
- name: Download Translations - name: Download translations
run: python3 -m script.translations download uses: actions/download-artifact@v4.1.4
env: with:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Write meta info file - name: Write meta info file
shell: bash shell: bash
@ -200,7 +199,7 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0 uses: docker/login-action@v3.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -216,17 +215,6 @@ jobs:
--target /data \ --target /data \
--generic ${{ needs.init.outputs.version }} --generic ${{ needs.init.outputs.version }}
- name: Archive translations
shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@v3
with:
name: translations
path: translations.tar.gz
if-no-files-found: error
build_machine: build_machine:
name: Build ${{ matrix.machine }} machine core image name: Build ${{ matrix.machine }} machine core image
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
@ -263,7 +251,7 @@ jobs:
- green - green
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set build additional args - name: Set build additional args
run: | run: |
@ -277,7 +265,7 @@ jobs:
fi fi
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0 uses: docker/login-action@v3.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -300,7 +288,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@ -336,9 +324,12 @@ jobs:
contents: read contents: read
packages: write packages: write
id-token: write id-token: write
strategy:
matrix:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.4.0 uses: sigstore/cosign-installer@v3.4.0
@ -346,13 +337,15 @@ jobs:
cosign-release: "v2.2.3" cosign-release: "v2.2.3"
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3.0.0 if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.1.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0 if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -366,41 +359,37 @@ jobs:
function create_manifest() { function create_manifest() {
local tag_l=${1} local tag_l=${1}
local tag_r=${2} local tag_r=${2}
local registry=${{ matrix.registry }}
for registry in "ghcr.io/home-assistant" "docker.io/homeassistant" docker manifest create "${registry}/home-assistant:${tag_l}" \
do "${registry}/amd64-homeassistant:${tag_r}" \
"${registry}/i386-homeassistant:${tag_r}" \
"${registry}/armhf-homeassistant:${tag_r}" \
"${registry}/armv7-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
docker manifest create "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \ "${registry}/amd64-homeassistant:${tag_r}" \
"${registry}/i386-homeassistant:${tag_r}" \ --os linux --arch amd64
"${registry}/armhf-homeassistant:${tag_r}" \
"${registry}/armv7-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \ "${registry}/i386-homeassistant:${tag_r}" \
--os linux --arch amd64 --os linux --arch 386
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/i386-homeassistant:${tag_r}" \ "${registry}/armhf-homeassistant:${tag_r}" \
--os linux --arch 386 --os linux --arch arm --variant=v6
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armhf-homeassistant:${tag_r}" \ "${registry}/armv7-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v6 --os linux --arch arm --variant=v7
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armv7-homeassistant:${tag_r}" \ "${registry}/aarch64-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v7 --os linux --arch arm64 --variant=v8
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest push --purge "${registry}/home-assistant:${tag_l}"
"${registry}/aarch64-homeassistant:${tag_r}" \ cosign sign --yes "${registry}/home-assistant:${tag_l}"
--os linux --arch arm64 --variant=v8
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
cosign sign --yes "${registry}/home-assistant:${tag_l}"
done
} }
function validate_image() { function validate_image() {
@ -433,12 +422,14 @@ jobs:
validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}" validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}" validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
# Upload images to dockerhub if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}" # Upload images to dockerhub
push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
fi
# Create version tag # Create version tag
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
@ -459,3 +450,44 @@ jobs:
v="${{ needs.init.outputs.version }}" v="${{ needs.init.outputs.version }}"
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}" create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
fi fi
build_python:
name: Build PyPi package
environment: ${{ needs.init.outputs.channel }}
needs: ["init", "build_base"]
runs-on: ubuntu-latest
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.1.4
with:
name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Build package
shell: bash
run: |
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install twine build
python -m build
- name: Upload package
shell: bash
run: |
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
twine upload dist/* --skip-existing

View File

@ -34,11 +34,11 @@ on:
env: env:
CACHE_VERSION: 5 CACHE_VERSION: 5
PIP_CACHE_VERSION: 4 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 7 MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.3" HA_SHORT_VERSION: "2024.4"
DEFAULT_PYTHON: "3.11" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.11', '3.12']" ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support # 10.6 is the current long-term-support
@ -56,7 +56,7 @@ env:
# - 15.2 is the latest (as of 9 Feb 2023) # - 15.2 is the latest (as of 9 Feb 2023)
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']" POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
PRE_COMMIT_CACHE: ~/.cache/pre-commit PRE_COMMIT_CACHE: ~/.cache/pre-commit
PIP_CACHE: /tmp/pip-cache UV_CACHE_DIR: /tmp/uv-cache
SQLALCHEMY_WARN_20: 1 SQLALCHEMY_WARN_20: 1
PYTHONASYNCIODEBUG: 1 PYTHONASYNCIODEBUG: 1
HASS_CI: 1 HASS_CI: 1
@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
id: generate_python_cache_key id: generate_python_cache_key
run: >- run: >-
@ -103,7 +103,7 @@ jobs:
echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{
hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Filter for core changes - name: Filter for core changes
uses: dorny/paths-filter@v3.0.1 uses: dorny/paths-filter@v3.0.2
id: core id: core
with: with:
filters: .core_files.yaml filters: .core_files.yaml
@ -118,7 +118,7 @@ jobs:
echo "Result:" echo "Result:"
cat .integration_paths.yaml cat .integration_paths.yaml
- name: Filter for integration changes - name: Filter for integration changes
uses: dorny/paths-filter@v3.0.1 uses: dorny/paths-filter@v3.0.2
id: integrations id: integrations
with: with:
filters: .integration_paths.yaml filters: .integration_paths.yaml
@ -222,16 +222,16 @@ jobs:
- info - info
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.0.0 uses: actions/cache@v4.0.2
with: with:
path: venv path: venv
key: >- key: >-
@ -243,10 +243,11 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install "$(cat requirements_test.txt | grep pre-commit)" pip install "$(grep '^uv' < requirements_test.txt)"
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.0.0 uses: actions/cache@v4.0.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true lookup-only: true
@ -267,16 +268,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -285,7 +286,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@ -307,16 +308,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -325,7 +326,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@ -346,16 +347,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -364,7 +365,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@ -440,37 +441,37 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Generate partial pip restore key - name: Generate partial uv restore key
id: generate-pip-key id: generate-uv-key
run: >- run: >-
echo "key=pip-${{ env.PIP_CACHE_VERSION }}-${{ echo "key=uv-${{ env.UV_CACHE_VERSION }}-${{
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.0.0 uses: actions/cache@v4.0.2
with: with:
path: venv path: venv
lookup-only: true lookup-only: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore pip wheel cache - name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.0.0 uses: actions/cache@v4.0.2
with: with:
path: ${{ env.PIP_CACHE }} path: ${{ env.UV_CACHE_DIR }}
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-pip-key.outputs.key }} steps.generate-uv-key.outputs.key }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ env.UV_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-
- name: Install additional OS dependencies - name: Install additional OS dependencies
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -492,10 +493,11 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
PIP_CACHE_DIR=$PIP_CACHE pip install -U "pip>=21.3.1" setuptools wheel pip install "$(grep '^uv' < requirements_test.txt)"
PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_all.txt uv pip install -U "pip>=21.3.1" setuptools wheel
PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_test.txt uv pip install -r requirements_all.txt
pip install -e . --config-settings editable_mode=compat uv pip install -r requirements_test.txt
uv pip install -e . --config-settings editable_mode=compat
hassfest: hassfest:
name: Check hassfest name: Check hassfest
@ -508,16 +510,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -540,16 +542,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -573,16 +575,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -617,10 +619,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -633,7 +635,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -641,7 +643,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore mypy cache - name: Restore mypy cache
uses: actions/cache@v4.0.0 uses: actions/cache@v4.0.2
with: with:
path: .mypy_cache path: .mypy_cache
key: >- key: >-
@ -699,16 +701,16 @@ jobs:
bluez \ bluez \
ffmpeg ffmpeg
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -717,13 +719,6 @@ jobs:
- name: Register Python problem matcher - name: Register Python problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/python.json" echo "::add-matcher::.github/workflows/matchers/python.json"
- name: Install Pytest Annotation plugin
run: |
. venv/bin/activate
# Ideally this should be part of our dependencies
# However this plugin is fairly new and doesn't run correctly
# on a non-GitHub environment.
pip install pytest-github-actions-annotate-failures==0.1.3
- name: Register pytest slow test problem matcher - name: Register pytest slow test problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
@ -797,10 +792,11 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure') if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure')
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v4.3.1
with: with:
name: pytest-${{ github.run_number }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.1 uses: actions/upload-artifact@v4.3.1
@ -852,16 +848,16 @@ jobs:
ffmpeg \ ffmpeg \
libmariadb-dev-compat libmariadb-dev-compat
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -870,20 +866,13 @@ jobs:
- name: Register Python problem matcher - name: Register Python problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/python.json" echo "::add-matcher::.github/workflows/matchers/python.json"
- name: Install Pytest Annotation plugin
run: |
. venv/bin/activate
# Ideally this should be part of our dependencies
# However this plugin is fairly new and doesn't run correctly
# on a non-GitHub environment.
pip install pytest-github-actions-annotate-failures==0.1.3
- name: Register pytest slow test problem matcher - name: Register pytest slow test problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Install SQL Python libraries - name: Install SQL Python libraries
run: | run: |
. venv/bin/activate . venv/bin/activate
pip install mysqlclient sqlalchemy_utils uv pip install mysqlclient sqlalchemy_utils
- name: Compile English translations - name: Compile English translations
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -923,10 +912,12 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v4.3.1
with: with:
name: pytest-${{ github.run_number }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.1 uses: actions/upload-artifact@v4.3.1
@ -979,16 +970,16 @@ jobs:
ffmpeg \ ffmpeg \
postgresql-server-dev-14 postgresql-server-dev-14
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.2
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@ -997,20 +988,13 @@ jobs:
- name: Register Python problem matcher - name: Register Python problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/python.json" echo "::add-matcher::.github/workflows/matchers/python.json"
- name: Install Pytest Annotation plugin
run: |
. venv/bin/activate
# Ideally this should be part of our dependencies
# However this plugin is fairly new and doesn't run correctly
# on a non-GitHub environment.
pip install pytest-github-actions-annotate-failures==0.1.3
- name: Register pytest slow test problem matcher - name: Register pytest slow test problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Install SQL Python libraries - name: Install SQL Python libraries
run: | run: |
. venv/bin/activate . venv/bin/activate
pip install psycopg2 sqlalchemy_utils uv pip install psycopg2 sqlalchemy_utils
- name: Compile English translations - name: Compile English translations
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -1051,10 +1035,12 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v4.3.1
with: with:
name: pytest-${{ github.run_number }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.1 uses: actions/upload-artifact@v4.3.1
@ -1077,14 +1063,14 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.3 uses: actions/download-artifact@v4.1.4
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov (full coverage) - name: Upload coverage to Codecov (full coverage)
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
uses: Wandalen/wretry.action@v1.4.4 uses: Wandalen/wretry.action@v2.1.0
with: with:
action: codecov/codecov-action@v3.1.3 action: codecov/codecov-action@v3.1.3
with: | with: |
@ -1095,7 +1081,7 @@ jobs:
attempt_delay: 30000 attempt_delay: 30000
- name: Upload coverage to Codecov (partial coverage) - name: Upload coverage to Codecov (partial coverage)
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
uses: Wandalen/wretry.action@v1.4.4 uses: Wandalen/wretry.action@v2.1.0
with: with:
action: codecov/codecov-action@v3.1.3 action: codecov/codecov-action@v3.1.3
with: | with: |

View File

@ -21,14 +21,14 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.24.5 uses: github/codeql-action/init@v3.24.9
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.24.5 uses: github/codeql-action/analyze@v3.24.9
with: with:
category: "/language:python" category: "/language:python"

View File

@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0 uses: actions/setup-python@v5.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -28,7 +28,7 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }} architectures: ${{ steps.info.outputs.architectures }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Get information - name: Get information
id: info id: info
@ -88,15 +88,15 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.3 uses: actions/download-artifact@v4.1.4
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.3 uses: actions/download-artifact@v4.1.4
with: with:
name: requirements_diff name: requirements_diff
@ -126,15 +126,15 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.3 uses: actions/download-artifact@v4.1.4
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.3 uses: actions/download-artifact@v4.1.4
with: with:
name: requirements_diff name: requirements_diff

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1 rev: v0.3.4
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -8,11 +8,11 @@ repos:
- id: ruff-format - id: ruff-format
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.2.2 rev: v2.2.6
hooks: hooks:
- id: codespell - id: codespell
args: args:
- --ignore-words-list=additionals,alle,alot,bund,currenty,datas,farenheit,falsy,fo,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,withing,zar - --ignore-words-list=additionals,alle,alot,astroid,bund,caf,convencional,currenty,datas,farenheit,falsy,fo,frequence,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,vor,withing,zar
- --skip="./.*,*.csv,*.json,*.ambr" - --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2 - --quiet-level=2
exclude_types: [csv, json] exclude_types: [csv, json]
@ -30,7 +30,7 @@ repos:
- --branch=master - --branch=master
- --branch=rc - --branch=rc
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.32.0 rev: v1.35.1
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier

View File

@ -1,6 +1,5 @@
*.md *.md
.strict-typing .strict-typing
azure-*.yml
homeassistant/components/*/translations/*.json homeassistant/components/*/translations/*.json
homeassistant/generated/* homeassistant/generated/*
tests/components/lidarr/fixtures/initialize.js tests/components/lidarr/fixtures/initialize.js

View File

@ -228,6 +228,7 @@ homeassistant.components.homekit_controller.select
homeassistant.components.homekit_controller.storage homeassistant.components.homekit_controller.storage
homeassistant.components.homekit_controller.utils homeassistant.components.homekit_controller.utils
homeassistant.components.homewizard.* homeassistant.components.homewizard.*
homeassistant.components.homeworks.*
homeassistant.components.http.* homeassistant.components.http.*
homeassistant.components.huawei_lte.* homeassistant.components.huawei_lte.*
homeassistant.components.humidifier.* homeassistant.components.humidifier.*

View File

@ -1,5 +1,4 @@
ignore: | ignore: |
azure-*.yml
tests/fixtures/core/config/yaml_errors/ tests/fixtures/core/config/yaml_errors/
rules: rules:
braces: braces:

View File

@ -309,14 +309,16 @@ build.json @home-assistant/supervisor
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket /tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery /homeassistant/components/dormakaba_dkey/ @emontnemery
/tests/components/dormakaba_dkey/ @emontnemery /tests/components/dormakaba_dkey/ @emontnemery
/homeassistant/components/downloader/ @erwindouna
/tests/components/downloader/ @erwindouna
/homeassistant/components/dremel_3d_printer/ @tkdrob /homeassistant/components/dremel_3d_printer/ @tkdrob
/tests/components/dremel_3d_printer/ @tkdrob /tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer /homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer /tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dsmr/ @Robbie1221 @frenck /homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @depl0y @glodenox /homeassistant/components/dsmr_reader/ @sorted-bits @glodenox
/tests/components/dsmr_reader/ @depl0y @glodenox /tests/components/dsmr_reader/ @sorted-bits @glodenox
/homeassistant/components/duotecno/ @cereal2nd /homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
@ -453,6 +455,8 @@ build.json @home-assistant/supervisor
/tests/components/frontier_silicon/ @wlcrs /tests/components/frontier_silicon/ @wlcrs
/homeassistant/components/fully_kiosk/ @cgarwood /homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fyta/ @dontinelli
/tests/components/fyta/ @dontinelli
/homeassistant/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus /homeassistant/components/gardena_bluetooth/ @elupus
@ -568,8 +572,8 @@ build.json @home-assistant/supervisor
/tests/components/homekit/ @bdraco /tests/components/homekit/ @bdraco
/homeassistant/components/homekit_controller/ @Jc2k @bdraco /homeassistant/components/homekit_controller/ @Jc2k @bdraco
/tests/components/homekit_controller/ @Jc2k @bdraco /tests/components/homekit_controller/ @Jc2k @bdraco
/homeassistant/components/homematic/ @pvizeli @danielperna84 /homeassistant/components/homematic/ @pvizeli
/tests/components/homematic/ @pvizeli @danielperna84 /tests/components/homematic/ @pvizeli
/homeassistant/components/homewizard/ @DCSBL /homeassistant/components/homewizard/ @DCSBL
/tests/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer /homeassistant/components/honeywell/ @rdfurman @mkmer
@ -837,6 +841,8 @@ build.json @home-assistant/supervisor
/tests/components/mopeka/ @bdraco /tests/components/mopeka/ @bdraco
/homeassistant/components/motion_blinds/ @starkillerOG /homeassistant/components/motion_blinds/ @starkillerOG
/tests/components/motion_blinds/ @starkillerOG /tests/components/motion_blinds/ @starkillerOG
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
/tests/components/motionblinds_ble/ @LennP @jerrybboy
/homeassistant/components/motioneye/ @dermotduffy /homeassistant/components/motioneye/ @dermotduffy
/tests/components/motioneye/ @dermotduffy /tests/components/motioneye/ @dermotduffy
/homeassistant/components/motionmount/ @RJPoelstra /homeassistant/components/motionmount/ @RJPoelstra
@ -860,8 +866,8 @@ build.json @home-assistant/supervisor
/tests/components/nam/ @bieniu /tests/components/nam/ @bieniu
/homeassistant/components/nanoleaf/ @milanmeu /homeassistant/components/nanoleaf/ @milanmeu
/tests/components/nanoleaf/ @milanmeu /tests/components/nanoleaf/ @milanmeu
/homeassistant/components/neato/ @dshokouhi @Santobert /homeassistant/components/neato/ @Santobert
/tests/components/neato/ @dshokouhi @Santobert /tests/components/neato/ @Santobert
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM /homeassistant/components/nederlandse_spoorwegen/ @YarmoM
/homeassistant/components/ness_alarm/ @nickw444 /homeassistant/components/ness_alarm/ @nickw444
/tests/components/ness_alarm/ @nickw444 /tests/components/ness_alarm/ @nickw444
@ -927,6 +933,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/octoprint/ @rfleming71 /homeassistant/components/octoprint/ @rfleming71
/tests/components/octoprint/ @rfleming71 /tests/components/octoprint/ @rfleming71
/homeassistant/components/ohmconnect/ @robbiet480 /homeassistant/components/ohmconnect/ @robbiet480
/homeassistant/components/ollama/ @synesthesiam
/tests/components/ollama/ @synesthesiam
/homeassistant/components/ombi/ @larssont /homeassistant/components/ombi/ @larssont
/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu /homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu
/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu /tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
@ -1095,7 +1103,6 @@ build.json @home-assistant/supervisor
/tests/components/recovery_mode/ @home-assistant/core /tests/components/recovery_mode/ @home-assistant/core
/homeassistant/components/refoss/ @ashionky /homeassistant/components/refoss/ @ashionky
/tests/components/refoss/ @ashionky /tests/components/refoss/ @ashionky
/homeassistant/components/rejseplanen/ @DarkFox
/homeassistant/components/remote/ @home-assistant/core /homeassistant/components/remote/ @home-assistant/core
/tests/components/remote/ @home-assistant/core /tests/components/remote/ @home-assistant/core
/homeassistant/components/renault/ @epenet /homeassistant/components/renault/ @epenet
@ -1191,6 +1198,8 @@ build.json @home-assistant/supervisor
/tests/components/senz/ @milanmeu /tests/components/senz/ @milanmeu
/homeassistant/components/serial/ @fabaff /homeassistant/components/serial/ @fabaff
/homeassistant/components/seven_segments/ @fabaff /homeassistant/components/seven_segments/ @fabaff
/homeassistant/components/seventeentrack/ @shaiu
/tests/components/seventeentrack/ @shaiu
/homeassistant/components/sfr_box/ @epenet /homeassistant/components/sfr_box/ @epenet
/tests/components/sfr_box/ @epenet /tests/components/sfr_box/ @epenet
/homeassistant/components/sharkiq/ @JeffResc @funkybunch /homeassistant/components/sharkiq/ @JeffResc @funkybunch

View File

@ -6,47 +6,47 @@ FROM ${BUILD_FROM}
# Synchronize with homeassistant/core.py:async_stop # Synchronize with homeassistant/core.py:async_stop
ENV \ ENV \
S6_SERVICES_GRACETIME=240000 S6_SERVICES_GRACETIME=240000 \
UV_SYSTEM_PYTHON=true
ARG QEMU_CPU ARG QEMU_CPU
# Install uv
RUN pip3 install uv==0.1.24
WORKDIR /usr/src WORKDIR /usr/src
## Setup Home Assistant Core dependencies ## Setup Home Assistant Core dependencies
COPY requirements.txt homeassistant/ COPY requirements.txt homeassistant/
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
RUN \ RUN \
pip3 install \ uv pip install \
--only-binary=:all: \ --no-build \
-r homeassistant/requirements.txt -r homeassistant/requirements.txt
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
RUN \ RUN \
if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
pip3 install homeassistant/home_assistant_frontend-*.whl; \ uv pip install homeassistant/home_assistant_*.whl; \
fi \
&& if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \
pip3 install homeassistant/home_assistant_intents-*.whl; \
fi \ fi \
&& if [ "${BUILD_ARCH}" = "i386" ]; then \ && if [ "${BUILD_ARCH}" = "i386" ]; then \
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
linux32 pip3 install \ linux32 uv pip install \
--only-binary=:all: \ --no-build \
-r homeassistant/requirements_all.txt; \ -r homeassistant/requirements_all.txt; \
else \ else \
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
pip3 install \ uv pip install \
--only-binary=:all: \ --no-build \
-r homeassistant/requirements_all.txt; \ -r homeassistant/requirements_all.txt; \
fi fi
## Setup Home Assistant Core ## Setup Home Assistant Core
COPY . homeassistant/ COPY . homeassistant/
RUN \ RUN \
pip3 install \ uv pip install \
--only-binary=:all: \
-e ./homeassistant \ -e ./homeassistant \
&& python3 -m compileall \ && python3 -m compileall \
homeassistant/homeassistant homeassistant/homeassistant

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11 FROM mcr.microsoft.com/devcontainers/python:1-3.12
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]

View File

@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant image: ghcr.io/home-assistant/{arch}-homeassistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.1 aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.03.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.1 armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.03.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.1 armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.03.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.1 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.03.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.1 i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.03.0
codenotary: codenotary:
signer: notary@home-assistant.io signer: notary@home-assistant.io
base_image: notary@home-assistant.io base_image: notary@home-assistant.io

View File

@ -1,4 +1,5 @@
"""Start Home Assistant.""" """Start Home Assistant."""
from __future__ import annotations from __future__ import annotations
import argparse import argparse

View File

@ -1,4 +1,5 @@
"""Provide an authentication layer for Home Assistant.""" """Provide an authentication layer for Home Assistant."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -19,13 +20,13 @@ from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback, callback,
) )
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import auth_store, jwt_wrapper, models from . import auth_store, jwt_wrapper, models
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
from .models import AuthFlowResult
from .providers import AuthProvider, LoginFlow, auth_provider_from_config from .providers import AuthProvider, LoginFlow, auth_provider_from_config
EVENT_USER_ADDED = "user_added" EVENT_USER_ADDED = "user_added"
@ -88,9 +89,13 @@ async def auth_manager_from_config(
return manager return manager
class AuthManagerFlowManager(data_entry_flow.FlowManager): class AuthManagerFlowManager(
data_entry_flow.FlowManager[AuthFlowResult, tuple[str, str]]
):
"""Manage authentication flows.""" """Manage authentication flows."""
_flow_result = AuthFlowResult
def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None: def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None:
"""Init auth manager flows.""" """Init auth manager flows."""
super().__init__(hass) super().__init__(hass)
@ -98,11 +103,11 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
async def async_create_flow( async def async_create_flow(
self, self,
handler_key: str, handler_key: tuple[str, str],
*, *,
context: dict[str, Any] | None = None, context: dict[str, Any] | None = None,
data: dict[str, Any] | None = None, data: dict[str, Any] | None = None,
) -> data_entry_flow.FlowHandler: ) -> LoginFlow:
"""Create a login flow.""" """Create a login flow."""
auth_provider = self.auth_manager.get_auth_provider(*handler_key) auth_provider = self.auth_manager.get_auth_provider(*handler_key)
if not auth_provider: if not auth_provider:
@ -110,8 +115,10 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
return await auth_provider.async_login_flow(context) return await auth_provider.async_login_flow(context)
async def async_finish_flow( async def async_finish_flow(
self, flow: data_entry_flow.FlowHandler, result: FlowResult self,
) -> FlowResult: flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]],
result: AuthFlowResult,
) -> AuthFlowResult:
"""Return a user as result of login flow.""" """Return a user as result of login flow."""
flow = cast(LoginFlow, flow) flow = cast(LoginFlow, flow)

View File

@ -1,4 +1,5 @@
"""Storage for auth models.""" """Storage for auth models."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
@ -30,6 +31,17 @@ GROUP_NAME_ADMIN = "Administrators"
GROUP_NAME_USER = "Users" GROUP_NAME_USER = "Users"
GROUP_NAME_READ_ONLY = "Read Only" GROUP_NAME_READ_ONLY = "Read Only"
# We always save the auth store after we load it since
# we may migrate data and do not want to have to do it again
# but we don't want to do it during startup so we schedule
# the first save 5 minutes out knowing something else may
# want to save the auth store before then, and since Storage
# will honor the lower of the two delays, it will save it
# faster if something else saves it.
INITIAL_LOAD_SAVE_DELAY = 300
DEFAULT_SAVE_DELAY = 1
class AuthStore: class AuthStore:
"""Stores authentication info. """Stores authentication info.
@ -467,12 +479,12 @@ class AuthStore:
self._groups = groups self._groups = groups
self._users = users self._users = users
self._async_schedule_save() self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
@callback @callback
def _async_schedule_save(self) -> None: def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None:
"""Save users.""" """Save users."""
self._store.async_delay_save(self._data_to_save, 1) self._store.async_delay_save(self._data_to_save, delay)
@callback @callback
def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:

View File

@ -1,4 +1,5 @@
"""Constants for the auth module.""" """Constants for the auth module."""
from datetime import timedelta from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)

View File

@ -4,6 +4,7 @@ Since we decode the same tokens over and over again
we can cache the result of the decode of valid tokens we can cache the result of the decode of valid tokens
to speed up the process. to speed up the process.
""" """
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta

View File

@ -1,7 +1,7 @@
"""Pluggable auth modules for Home Assistant.""" """Pluggable auth modules for Home Assistant."""
from __future__ import annotations from __future__ import annotations
import importlib
import logging import logging
import types import types
from typing import Any from typing import Any
@ -14,6 +14,7 @@ from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.importlib import async_import_module
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry() MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
@ -148,7 +149,7 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul
module_path = f"homeassistant.auth.mfa_modules.{module_name}" module_path = f"homeassistant.auth.mfa_modules.{module_name}"
try: try:
module = importlib.import_module(module_path) module = await async_import_module(hass, module_path)
except ImportError as err: except ImportError as err:
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err) _LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
raise HomeAssistantError( raise HomeAssistantError(

View File

@ -1,4 +1,5 @@
"""Example auth module.""" """Example auth module."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -2,6 +2,7 @@
Sending HOTP through notify service Sending HOTP through notify service
""" """
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio

View File

@ -1,4 +1,5 @@
"""Time-based One Time Password auth module.""" """Time-based One Time Password auth module."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio

View File

@ -1,4 +1,5 @@
"""Auth models.""" """Auth models."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -11,6 +12,7 @@ from attr import Attribute
from attr.setters import validate from attr.setters import validate
from homeassistant.const import __version__ from homeassistant.const import __version__
from homeassistant.data_entry_flow import FlowResult
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import permissions as perm_mdl from . import permissions as perm_mdl
@ -26,6 +28,8 @@ TOKEN_TYPE_NORMAL = "normal"
TOKEN_TYPE_SYSTEM = "system" TOKEN_TYPE_SYSTEM = "system"
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
AuthFlowResult = FlowResult[tuple[str, str]]
@attr.s(slots=True) @attr.s(slots=True)
class Group: class Group:

View File

@ -1,4 +1,5 @@
"""Permissions for Home Assistant.""" """Permissions for Home Assistant."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable

View File

@ -1,4 +1,5 @@
"""Permission constants.""" """Permission constants."""
CAT_ENTITIES = "entities" CAT_ENTITIES = "entities"
CAT_CONFIG_ENTRIES = "config_entries" CAT_CONFIG_ENTRIES = "config_entries"
SUBCAT_ALL = "all" SUBCAT_ALL = "all"

View File

@ -1,4 +1,5 @@
"""Entity permissions.""" """Entity permissions."""
from __future__ import annotations from __future__ import annotations
from collections import OrderedDict from collections import OrderedDict

View File

@ -1,4 +1,5 @@
"""Permission for events.""" """Permission for events."""
from __future__ import annotations from __future__ import annotations
from typing import Final from typing import Final

View File

@ -1,4 +1,5 @@
"""Merging of policies.""" """Merging of policies."""
from __future__ import annotations from __future__ import annotations
from typing import cast from typing import cast
@ -57,10 +58,7 @@ def _merge_policies(sources: list[CategoryType]) -> CategoryType:
continue continue
seen.add(key) seen.add(key)
key_sources = [] key_sources = [src.get(key) for src in sources if isinstance(src, dict)]
for src in sources:
if isinstance(src, dict):
key_sources.append(src.get(key))
policy[key] = _merge_policies(key_sources) policy[key] = _merge_policies(key_sources)

View File

@ -1,4 +1,5 @@
"""Models for permissions.""" """Models for permissions."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING

View File

@ -1,4 +1,5 @@
"""System policies.""" """System policies."""
from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL
ADMIN_POLICY = {CAT_ENTITIES: True} ADMIN_POLICY = {CAT_ENTITIES: True}

View File

@ -1,4 +1,5 @@
"""Common code for permissions.""" """Common code for permissions."""
from collections.abc import Mapping from collections.abc import Mapping
# MyPy doesn't support recursion yet. So writing it out as far as we need. # MyPy doesn't support recursion yet. So writing it out as far as we need.

View File

@ -1,4 +1,5 @@
"""Helpers to deal with permissions.""" """Helpers to deal with permissions."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable

View File

@ -1,8 +1,8 @@
"""Auth providers for Home Assistant.""" """Auth providers for Home Assistant."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
import importlib
import logging import logging
import types import types
from typing import Any from typing import Any
@ -13,14 +13,14 @@ from voluptuous.humanize import humanize_error
from homeassistant import data_entry_flow, requirements from homeassistant import data_entry_flow, requirements
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.importlib import async_import_module
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from ..auth_store import AuthStore from ..auth_store import AuthStore
from ..const import MFA_SESSION_EXPIRATION from ..const import MFA_SESSION_EXPIRATION
from ..models import Credentials, RefreshToken, User, UserMeta from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_REQS = "auth_prov_reqs_processed" DATA_REQS = "auth_prov_reqs_processed"
@ -157,7 +157,9 @@ async def load_auth_provider_module(
) -> types.ModuleType: ) -> types.ModuleType:
"""Load an auth provider.""" """Load an auth provider."""
try: try:
module = importlib.import_module(f"homeassistant.auth.providers.{provider}") module = await async_import_module(
hass, f"homeassistant.auth.providers.{provider}"
)
except ImportError as err: except ImportError as err:
_LOGGER.error("Unable to load auth provider %s: %s", provider, err) _LOGGER.error("Unable to load auth provider %s: %s", provider, err)
raise HomeAssistantError( raise HomeAssistantError(
@ -181,9 +183,11 @@ async def load_auth_provider_module(
return module return module
class LoginFlow(data_entry_flow.FlowHandler): class LoginFlow(data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]]):
"""Handler for the login flow.""" """Handler for the login flow."""
_flow_result = AuthFlowResult
def __init__(self, auth_provider: AuthProvider) -> None: def __init__(self, auth_provider: AuthProvider) -> None:
"""Initialize the login flow.""" """Initialize the login flow."""
self._auth_provider = auth_provider self._auth_provider = auth_provider
@ -197,7 +201,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the first step of login flow. """Handle the first step of login flow.
Return self.async_show_form(step_id='init') if user_input is None. Return self.async_show_form(step_id='init') if user_input is None.
@ -207,7 +211,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
async def async_step_select_mfa_module( async def async_step_select_mfa_module(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the step of select mfa module.""" """Handle the step of select mfa module."""
errors = {} errors = {}
@ -232,7 +236,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
async def async_step_mfa( async def async_step_mfa(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the step of mfa validation.""" """Handle the step of mfa validation."""
assert self.credential assert self.credential
assert self.user assert self.user
@ -282,6 +286,6 @@ class LoginFlow(data_entry_flow.FlowHandler):
errors=errors, errors=errors,
) )
async def async_finish(self, flow_result: Any) -> FlowResult: async def async_finish(self, flow_result: Any) -> AuthFlowResult:
"""Handle the pass of login flow.""" """Handle the pass of login flow."""
return self.async_create_entry(data=flow_result) return self.async_create_entry(data=flow_result)

View File

@ -1,4 +1,5 @@
"""Auth provider that validates credentials via an external command.""" """Auth provider that validates credentials via an external command."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -10,10 +11,9 @@ from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_COMMAND from homeassistant.const import CONF_COMMAND
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from ..models import Credentials, UserMeta from ..models import AuthFlowResult, Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
CONF_ARGS = "args" CONF_ARGS = "args"
@ -138,7 +138,7 @@ class CommandLineLoginFlow(LoginFlow):
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}

View File

@ -1,4 +1,5 @@
"""Home Assistant auth provider.""" """Home Assistant auth provider."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -12,11 +13,10 @@ import voluptuous as vol
from homeassistant.const import CONF_ID from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from ..models import Credentials, UserMeta from ..models import AuthFlowResult, Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
STORAGE_VERSION = 1 STORAGE_VERSION = 1
@ -321,7 +321,7 @@ class HassLoginFlow(LoginFlow):
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}

View File

@ -1,4 +1,5 @@
"""Example auth provider.""" """Example auth provider."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
@ -8,10 +9,9 @@ from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from ..models import Credentials, UserMeta from ..models import AuthFlowResult, Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
USER_SCHEMA = vol.Schema( USER_SCHEMA = vol.Schema(
@ -98,7 +98,7 @@ class ExampleLoginFlow(LoginFlow):
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = None errors = None

View File

@ -2,6 +2,7 @@
It will be removed when auth system production ready It will be removed when auth system production ready
""" """
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
@ -11,12 +12,11 @@ from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.core import async_get_hass, callback from homeassistant.core import async_get_hass, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from ..models import Credentials, UserMeta from ..models import AuthFlowResult, Credentials, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
AUTH_PROVIDER_TYPE = "legacy_api_password" AUTH_PROVIDER_TYPE = "legacy_api_password"
@ -101,7 +101,7 @@ class LegacyLoginFlow(LoginFlow):
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}

View File

@ -3,6 +3,7 @@
It shows list of users if access from trusted network. It shows list of users if access from trusted network.
Abort login flow if not access from trusted network. Abort login flow if not access from trusted network.
""" """
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
@ -19,13 +20,12 @@ from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.network import is_cloud_connection from homeassistant.helpers.network import is_cloud_connection
from .. import InvalidAuthError from .. import InvalidAuthError
from ..models import Credentials, RefreshToken, UserMeta from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
IPAddress = IPv4Address | IPv6Address IPAddress = IPv4Address | IPv6Address
@ -226,7 +226,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> FlowResult: ) -> AuthFlowResult:
"""Handle the step of the form.""" """Handle the step of the form."""
try: try:
cast( cast(

View File

@ -6,6 +6,7 @@ Since we have dropped support for Python 3.10, we can remove this backport.
This file is kept for now to avoid breaking custom components that might This file is kept for now to avoid breaking custom components that might
import it. import it.
""" """
from __future__ import annotations from __future__ import annotations
from enum import StrEnum from enum import StrEnum

View File

@ -41,12 +41,10 @@ class cached_property(Generic[_T]):
) )
@overload @overload
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ...
...
@overload @overload
def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: ...
...
def __get__( def __get__(
self, instance: Any | None, owner: type[Any] | None = None self, instance: Any | None, owner: type[Any] | None = None

View File

@ -1,4 +1,5 @@
"""Block blocking calls being done in asyncio.""" """Block blocking calls being done in asyncio."""
from http.client import HTTPConnection from http.client import HTTPConnection
import time import time

View File

@ -1,12 +1,15 @@
"""Provide methods to bootstrap a Home Assistant instance.""" """Provide methods to bootstrap a Home Assistant instance."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections import defaultdict
import contextlib import contextlib
from datetime import timedelta from functools import partial
from itertools import chain
import logging import logging
import logging.handlers import logging.handlers
from operator import itemgetter from operator import contains, itemgetter
import os import os
import platform import platform
import sys import sys
@ -22,17 +25,35 @@ import yarl
from . import config as conf_util, config_entries, core, loader, requirements from . import config as conf_util, config_entries, core, loader, requirements
# Pre-import config and lovelace which have no requirements here to avoid # Pre-import frontend deps which have no requirements here to avoid
# loading them at run time and blocking the event loop. We do this ahead # loading them at run time and blocking the event loop. We do this ahead
# of time so that we do not have to flag frontends deps with `import_executor` # of time so that we do not have to flag frontend deps with `import_executor`
# as it would create a thundering heard of executor jobs trying to import # as it would create a thundering heard of executor jobs trying to import
# frontend deps at the same time. # frontend deps at the same time.
from .components import ( from .components import (
api as api_pre_import, # noqa: F401 api as api_pre_import, # noqa: F401
auth as auth_pre_import, # noqa: F401
config as config_pre_import, # noqa: F401 config as config_pre_import, # noqa: F401
http, default_config as default_config_pre_import, # noqa: F401
device_automation as device_automation_pre_import, # noqa: F401
diagnostics as diagnostics_pre_import, # noqa: F401
file_upload as file_upload_pre_import, # noqa: F401
group as group_pre_import, # noqa: F401
history as history_pre_import, # noqa: F401
http, # not named pre_import since it has requirements
image_upload as image_upload_import, # noqa: F401 - not named pre_import since it has requirements
logbook as logbook_pre_import, # noqa: F401
lovelace as lovelace_pre_import, # noqa: F401 lovelace as lovelace_pre_import, # noqa: F401
onboarding as onboarding_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
sensor as sensor_pre_import, # noqa: F401
system_log as system_log_pre_import, # noqa: F401
webhook as webhook_pre_import, # noqa: F401
websocket_api as websocket_api_pre_import, # noqa: F401
) )
from .components.sensor import recorder as sensor_recorder # noqa: F401
from .const import ( from .const import (
FORMAT_DATETIME, FORMAT_DATETIME,
KEY_DATA_LOGGING as DATA_LOGGING, KEY_DATA_LOGGING as DATA_LOGGING,
@ -43,6 +64,7 @@ from .const import (
from .exceptions import HomeAssistantError from .exceptions import HomeAssistantError
from .helpers import ( from .helpers import (
area_registry, area_registry,
category_registry,
config_validation as cv, config_validation as cv,
device_registry, device_registry,
entity, entity,
@ -56,11 +78,13 @@ from .helpers import (
translation, translation,
) )
from .helpers.dispatcher import async_dispatcher_send from .helpers.dispatcher import async_dispatcher_send
from .helpers.storage import get_internal_store_manager
from .helpers.system_info import async_get_system_info
from .helpers.typing import ConfigType from .helpers.typing import ConfigType
from .setup import ( from .setup import (
BASE_PLATFORMS, BASE_PLATFORMS,
DATA_SETUP_STARTED, DATA_SETUP_STARTED,
DATA_SETUP_TIME, async_get_setup_timings,
async_notify_setup_error, async_notify_setup_error,
async_set_domains_to_be_loaded, async_set_domains_to_be_loaded,
async_setup_component, async_setup_component,
@ -69,11 +93,19 @@ from .util.async_ import create_eager_task
from .util.logging import async_activate_log_queue_handler from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_virtual_env from .util.package import async_get_user_site, is_virtual_env
with contextlib.suppress(ImportError):
# Ensure anyio backend is imported to avoid it being imported in the event loop
from anyio._backends import _asyncio # noqa: F401
if TYPE_CHECKING: if TYPE_CHECKING:
from .runner import RuntimeConfig from .runner import RuntimeConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS)
ERROR_LOG_FILENAME = "home-assistant.log" ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information. # hass.data key for logging information.
@ -87,7 +119,6 @@ STAGE_2_TIMEOUT = 300
WRAP_UP_TIMEOUT = 300 WRAP_UP_TIMEOUT = 300
COOLDOWN_TIME = 60 COOLDOWN_TIME = 60
MAX_LOAD_CONCURRENTLY = 6
DEBUGGER_INTEGRATIONS = {"debugpy"} DEBUGGER_INTEGRATIONS = {"debugpy"}
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"} CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
@ -128,6 +159,7 @@ DEFAULT_INTEGRATIONS = {
# These integrations are set up unless recovery mode is activated. # These integrations are set up unless recovery mode is activated.
# #
# Integrations providing core functionality: # Integrations providing core functionality:
"analytics", # Needed for onboarding
"application_credentials", "application_credentials",
"backup", "backup",
"frontend", "frontend",
@ -168,16 +200,35 @@ CRITICAL_INTEGRATIONS = {
"frontend", "frontend",
} }
SETUP_ORDER = { SETUP_ORDER = (
# Load logging as soon as possible # Load logging as soon as possible
"logging": LOGGING_INTEGRATIONS, ("logging", LOGGING_INTEGRATIONS),
# Setup frontend # Setup frontend and recorder
"frontend": FRONTEND_INTEGRATIONS, ("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
# Setup recorder
"recorder": RECORDER_INTEGRATIONS,
# Start up debuggers. Start these first in case they want to wait. # Start up debuggers. Start these first in case they want to wait.
"debugger": DEBUGGER_INTEGRATIONS, ("debugger", DEBUGGER_INTEGRATIONS),
} )
#
# Storage keys we are likely to load during startup
# in order of when we expect to load them.
#
# If they do not exist they will not be loaded
#
PRELOAD_STORAGE = [
"core.network",
"http.auth",
"image",
"lovelace_dashboards",
"lovelace_resources",
"core.uuid",
"lovelace.map",
"bluetooth.passive_update_processor",
"bluetooth.remote_scanners",
"assist_pipeline.pipelines",
"core.analytics",
"auth_module.totp",
]
async def async_setup_hass( async def async_setup_hass(
@ -315,14 +366,16 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
asyncio event loop. By primeing the cache of uname we can asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop. avoid the blocking call in the event loop.
""" """
platform.uname().processor # pylint: disable=expression-not-assigned _ = platform.uname().processor
# Load the registries and cache the result of platform.uname().processor # Load the registries and cache the result of platform.uname().processor
translation.async_setup(hass) translation.async_setup(hass)
entity.async_setup(hass) entity.async_setup(hass)
template.async_setup(hass) template.async_setup(hass)
await asyncio.gather( await asyncio.gather(
create_eager_task(get_internal_store_manager(hass).async_initialize()),
create_eager_task(area_registry.async_load(hass)), create_eager_task(area_registry.async_load(hass)),
create_eager_task(category_registry.async_load(hass)),
create_eager_task(device_registry.async_load(hass)), create_eager_task(device_registry.async_load(hass)),
create_eager_task(entity_registry.async_load(hass)), create_eager_task(entity_registry.async_load(hass)),
create_eager_task(floor_registry.async_load(hass)), create_eager_task(floor_registry.async_load(hass)),
@ -332,6 +385,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
create_eager_task(template.async_load_custom_templates(hass)), create_eager_task(template.async_load_custom_templates(hass)),
create_eager_task(restore_state.async_load(hass)), create_eager_task(restore_state.async_load(hass)),
create_eager_task(hass.config_entries.async_initialize()), create_eager_task(hass.config_entries.async_initialize()),
create_eager_task(async_get_system_info(hass)),
) )
@ -571,7 +625,9 @@ class _WatchPendingSetups:
"""Periodic log and dispatch of setups that are pending.""" """Periodic log and dispatch of setups that are pending."""
def __init__( def __init__(
self, hass: core.HomeAssistant, setup_started: dict[str, float] self,
hass: core.HomeAssistant,
setup_started: dict[tuple[str, str | None], float],
) -> None: ) -> None:
"""Initialize the WatchPendingSetups class.""" """Initialize the WatchPendingSetups class."""
self._hass = hass self._hass = hass
@ -586,11 +642,15 @@ class _WatchPendingSetups:
now = monotonic() now = monotonic()
self._duration_count += SLOW_STARTUP_CHECK_INTERVAL self._duration_count += SLOW_STARTUP_CHECK_INTERVAL
remaining_with_setup_started = { remaining_with_setup_started: defaultdict[str, float] = defaultdict(float)
domain: (now - start_time) for integration_group, start_time in self._setup_started.items():
for domain, start_time in self._setup_started.items() domain, _ = integration_group
} remaining_with_setup_started[domain] += now - start_time
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
if remaining_with_setup_started:
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
elif waiting_tasks := self._hass._active_tasks: # pylint: disable=protected-access
_LOGGER.debug("Waiting on tasks: %s", waiting_tasks)
self._async_dispatch(remaining_with_setup_started) self._async_dispatch(remaining_with_setup_started)
if ( if (
self._setup_started self._setup_started
@ -600,7 +660,7 @@ class _WatchPendingSetups:
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up # once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
_LOGGER.warning( _LOGGER.warning(
"Waiting on integrations to complete setup: %s", "Waiting on integrations to complete setup: %s",
", ".join(self._setup_started), self._setup_started,
) )
_LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones) _LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones)
@ -640,13 +700,18 @@ async def async_setup_multi_components(
"""Set up multiple domains. Log on failure.""" """Set up multiple domains. Log on failure."""
# Avoid creating tasks for domains that were setup in a previous stage # Avoid creating tasks for domains that were setup in a previous stage
domains_not_yet_setup = domains - hass.config.components 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.
futures = { futures = {
domain: hass.async_create_task( domain: hass.async_create_task(
async_setup_component(hass, domain, config), async_setup_component(hass, domain, config),
f"setup component {domain}", f"setup component {domain}",
eager_start=True, eager_start=True,
) )
for domain in domains_not_yet_setup for domain in sorted(
domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True
)
} }
results = await asyncio.gather(*futures.values(), return_exceptions=True) results = await asyncio.gather(*futures.values(), return_exceptions=True)
for idx, domain in enumerate(futures): for idx, domain in enumerate(futures):
@ -663,26 +728,53 @@ async def _async_resolve_domains_to_setup(
hass: core.HomeAssistant, config: dict[str, Any] hass: core.HomeAssistant, config: dict[str, Any]
) -> tuple[set[str], dict[str, loader.Integration]]: ) -> tuple[set[str], dict[str, loader.Integration]]:
"""Resolve all dependencies and return list of domains to set up.""" """Resolve all dependencies and return list of domains to set up."""
base_platforms_loaded = False
domains_to_setup = _get_domains(hass, config) domains_to_setup = _get_domains(hass, config)
needed_requirements: set[str] = set() needed_requirements: set[str] = set()
platform_integrations = conf_util.extract_platform_integrations(
config, BASE_PLATFORMS
)
# Ensure base platforms that have platform integrations are added to
# to `domains_to_setup so they can be setup first instead of
# discovering them when later when a config entry setup task
# notices its needed and there is already a long line to use
# the import executor.
#
# For example if we have
# sensor:
# - platform: template
#
# `template` has to be loaded to validate the config for sensor
# so we want to start loading `sensor` as soon as we know
# it will be needed. The more platforms under `sensor:`, the longer
# it will take to finish setup for `sensor` because each of these
# platforms has to be imported before we can validate the config.
#
# Thankfully we are migrating away from the platform pattern
# so this will be less of a problem in the future.
domains_to_setup.update(platform_integrations)
# Load manifests for base platforms and platform based integrations
# that are defined under base platforms right away since we do not require
# the manifest to list them as dependencies and we want to avoid the lock
# contention when multiple integrations try to load them at once
additional_manifests_to_load = {
*BASE_PLATFORMS,
*chain.from_iterable(platform_integrations.values()),
}
translations_to_load = additional_manifests_to_load.copy()
# Resolve all dependencies so we know all integrations # Resolve all dependencies so we know all integrations
# that will have to be loaded and start rightaway # that will have to be loaded and start right-away
integration_cache: dict[str, loader.Integration] = {} integration_cache: dict[str, loader.Integration] = {}
to_resolve: set[str] = domains_to_setup to_resolve: set[str] = domains_to_setup
while to_resolve: while to_resolve or additional_manifests_to_load:
old_to_resolve: set[str] = to_resolve old_to_resolve: set[str] = to_resolve
to_resolve = set() to_resolve = set()
if not base_platforms_loaded: if additional_manifests_to_load:
# Load base platforms right away since to_get = {*old_to_resolve, *additional_manifests_to_load}
# we do not require the manifest to list additional_manifests_to_load.clear()
# them as dependencies and we want
# to avoid the lock contention when multiple
# integrations try to resolve them at once
base_platforms_loaded = True
to_get = {*old_to_resolve, *BASE_PLATFORMS}
else: else:
to_get = old_to_resolve to_get = old_to_resolve
@ -691,13 +783,27 @@ async def _async_resolve_domains_to_setup(
integrations_to_process: list[loader.Integration] = [] integrations_to_process: list[loader.Integration] = []
for domain, itg in (await loader.async_get_integrations(hass, to_get)).items(): for domain, itg in (await loader.async_get_integrations(hass, to_get)).items():
if not isinstance(itg, loader.Integration) or domain not in old_to_resolve: if not isinstance(itg, loader.Integration):
continue continue
integrations_to_process.append(itg)
integration_cache[domain] = itg 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.dependencies)
manifest_deps.update(itg.after_dependencies) manifest_deps.update(itg.after_dependencies)
needed_requirements.update(itg.requirements)
if not itg.all_dependencies_resolved: if not itg.all_dependencies_resolved:
resolve_dependencies_tasks.append( resolve_dependencies_tasks.append(
create_eager_task( create_eager_task(
@ -740,6 +846,12 @@ async def _async_resolve_domains_to_setup(
"check installed requirements", "check installed requirements",
eager_start=True, 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 # 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 # 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 # lot of waiting for the translation load lock and a thundering herd of
@ -751,11 +863,22 @@ async def _async_resolve_domains_to_setup(
# wait for the translation load lock, loading will be done by the # wait for the translation load lock, loading will be done by the
# time it gets to it. # time it gets to it.
hass.async_create_background_task( hass.async_create_background_task(
translation.async_load_integrations(hass, {*BASE_PLATFORMS, *domains_to_setup}), translation.async_load_integrations(hass, translations_to_load),
"load translations", "load translations",
eager_start=True, eager_start=True,
) )
# Preload storage for all integrations we are going to set up
# so we do not have to wait for it to be loaded when we need it
# in the setup process.
hass.async_create_background_task(
get_internal_store_manager(hass).async_preload(
[*PRELOAD_STORAGE, *domains_to_setup]
),
"preload storage",
eager_start=True,
)
return domains_to_setup, integration_cache return domains_to_setup, integration_cache
@ -763,10 +886,8 @@ async def _async_set_up_integrations(
hass: core.HomeAssistant, config: dict[str, Any] hass: core.HomeAssistant, config: dict[str, Any]
) -> None: ) -> None:
"""Set up all the integrations.""" """Set up all the integrations."""
setup_started: dict[str, float] = {} setup_started: dict[tuple[str, str | None], float] = {}
hass.data[DATA_SETUP_STARTED] = setup_started hass.data[DATA_SETUP_STARTED] = setup_started
setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {})
watcher = _WatchPendingSetups(hass, setup_started) watcher = _WatchPendingSetups(hass, setup_started)
watcher.async_start() watcher.async_start()
@ -778,10 +899,9 @@ async def _async_set_up_integrations(
if "recorder" in domains_to_setup: if "recorder" in domains_to_setup:
recorder.async_initialize_recorder(hass) recorder.async_initialize_recorder(hass)
pre_stage_domains: dict[str, set[str]] = { pre_stage_domains = [
name: domains_to_setup & domain_group (name, domains_to_setup & domain_group) for name, domain_group in SETUP_ORDER
for name, domain_group in SETUP_ORDER.items() ]
}
# calculate what components to setup in what stage # calculate what components to setup in what stage
stage_1_domains: set[str] = set() stage_1_domains: set[str] = set()
@ -807,10 +927,18 @@ async def _async_set_up_integrations(
stage_2_domains = domains_to_setup - stage_1_domains stage_2_domains = domains_to_setup - stage_1_domains
for name, domain_group in pre_stage_domains.items(): for name, domain_group in pre_stage_domains:
if domain_group: if domain_group:
stage_2_domains -= domain_group stage_2_domains -= domain_group
_LOGGER.info("Setting up %s: %s", name, 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) await async_setup_multi_components(hass, domain_group, config)
# Enables after dependencies when setting up stage 1 domains # Enables after dependencies when setting up stage 1 domains
@ -825,7 +953,10 @@ async def _async_set_up_integrations(
): ):
await async_setup_multi_components(hass, stage_1_domains, config) await async_setup_multi_components(hass, stage_1_domains, config)
except TimeoutError: except TimeoutError:
_LOGGER.warning("Setup timed out for stage 1 - moving forward") _LOGGER.warning(
"Setup timed out for stage 1 waiting on %s - moving forward",
hass._active_tasks, # pylint: disable=protected-access
)
# Add after dependencies when setting up stage 2 domains # Add after dependencies when setting up stage 2 domains
async_set_domains_to_be_loaded(hass, stage_2_domains) async_set_domains_to_be_loaded(hass, stage_2_domains)
@ -838,7 +969,10 @@ async def _async_set_up_integrations(
): ):
await async_setup_multi_components(hass, stage_2_domains, config) await async_setup_multi_components(hass, stage_2_domains, config)
except TimeoutError: except TimeoutError:
_LOGGER.warning("Setup timed out for stage 2 - moving forward") _LOGGER.warning(
"Setup timed out for stage 2 waiting on %s - moving forward",
hass._active_tasks, # pylint: disable=protected-access
)
# Wrap up startup # Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up") _LOGGER.debug("Waiting for startup to wrap up")
@ -846,11 +980,16 @@ async def _async_set_up_integrations(
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME): async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
await hass.async_block_till_done() await hass.async_block_till_done()
except TimeoutError: except TimeoutError:
_LOGGER.warning("Setup timed out for bootstrap - moving forward") _LOGGER.warning(
"Setup timed out for bootstrap waiting on %s - moving forward",
hass._active_tasks, # pylint: disable=protected-access
)
watcher.async_stop() watcher.async_stop()
_LOGGER.debug( if _LOGGER.isEnabledFor(logging.DEBUG):
"Integration setup times: %s", setup_time = async_get_setup_timings(hass)
dict(sorted(setup_time.items(), key=itemgetter(1))), _LOGGER.debug(
) "Integration setup times: %s",
dict(sorted(setup_time.items(), key=itemgetter(1), reverse=True)),
)

View File

@ -0,0 +1,5 @@
{
"domain": "motionblinds",
"name": "Motionblinds",
"integrations": ["motion_blinds", "motionblinds_ble"]
}

View File

@ -6,11 +6,13 @@ Component design guidelines:
format "<DOMAIN>.<OBJECT_ID>". format "<DOMAIN>.<OBJECT_ID>".
- Each component should publish services only under its own domain. - Each component should publish services only under its own domain.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers.frame import report
from homeassistant.helpers.group import expand_entity_ids from homeassistant.helpers.group import expand_entity_ids
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,6 +23,15 @@ def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
If there is no entity id given we will check all. If there is no entity id given we will check all.
""" """
report(
(
"uses homeassistant.components.is_on."
" This is deprecated and will stop working in Home Assistant 2024.9, it"
" should be updated to use the function of the platform directly."
),
error_if_core=True,
)
if entity_id: if entity_id:
entity_ids = expand_entity_ids(hass, [entity_id]) entity_ids = expand_entity_ids(hass, [entity_id])
else: else:

View File

@ -1,4 +1,5 @@
"""Support for the Abode Security System.""" """Support for the Abode Security System."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field

View File

@ -1,10 +1,13 @@
"""Support for Abode Security System alarm control panels.""" """Support for Abode Security System alarm control panels."""
from __future__ import annotations from __future__ import annotations
from jaraco.abode.devices.alarm import Alarm as AbodeAl from jaraco.abode.devices.alarm import Alarm
import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import (
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
@ -28,7 +31,7 @@ async def async_setup_entry(
) )
class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity):
"""An alarm_control_panel implementation for Abode.""" """An alarm_control_panel implementation for Abode."""
_attr_name = None _attr_name = None
@ -37,7 +40,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
AlarmControlPanelEntityFeature.ARM_HOME AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_AWAY
) )
_device: AbodeAl _device: Alarm
@property @property
def state(self) -> str | None: def state(self) -> str | None:

View File

@ -1,10 +1,17 @@
"""Support for Abode Security System binary sensors.""" """Support for Abode Security System binary sensors."""
from __future__ import annotations from __future__ import annotations
from typing import cast from typing import cast
from jaraco.abode.devices.sensor import BinarySensor as ABBinarySensor from jaraco.abode.devices.sensor import BinarySensor
from jaraco.abode.helpers import constants as CONST from jaraco.abode.helpers.constants import (
TYPE_CONNECTIVITY,
TYPE_MOISTURE,
TYPE_MOTION,
TYPE_OCCUPANCY,
TYPE_OPENING,
)
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
@ -26,11 +33,11 @@ async def async_setup_entry(
data: AbodeSystem = hass.data[DOMAIN] data: AbodeSystem = hass.data[DOMAIN]
device_types = [ device_types = [
CONST.TYPE_CONNECTIVITY, TYPE_CONNECTIVITY,
CONST.TYPE_MOISTURE, TYPE_MOISTURE,
CONST.TYPE_MOTION, TYPE_MOTION,
CONST.TYPE_OCCUPANCY, TYPE_OCCUPANCY,
CONST.TYPE_OPENING, TYPE_OPENING,
] ]
async_add_entities( async_add_entities(
@ -43,7 +50,7 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorEntity):
"""A binary sensor implementation for Abode device.""" """A binary sensor implementation for Abode device."""
_attr_name = None _attr_name = None
_device: ABBinarySensor _device: BinarySensor
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:

View File

@ -1,12 +1,14 @@
"""Support for Abode Security System cameras.""" """Support for Abode Security System cameras."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import Any, cast from typing import Any, cast
from jaraco.abode.devices.base import Device as AbodeDev from jaraco.abode.devices.base import Device
from jaraco.abode.devices.camera import Camera as AbodeCam from jaraco.abode.devices.camera import Camera as AbodeCam
from jaraco.abode.helpers import constants as CONST, timeline as TIMELINE from jaraco.abode.helpers import timeline
from jaraco.abode.helpers.constants import TYPE_CAMERA
import requests import requests
from requests.models import Response from requests.models import Response
@ -30,8 +32,8 @@ async def async_setup_entry(
data: AbodeSystem = hass.data[DOMAIN] data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) AbodeCamera(data, device, timeline.CAPTURE_IMAGE)
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) for device in data.abode.get_devices(generic_type=TYPE_CAMERA)
) )
@ -41,7 +43,7 @@ class AbodeCamera(AbodeDevice, Camera):
_device: AbodeCam _device: AbodeCam
_attr_name = None _attr_name = None
def __init__(self, data: AbodeSystem, device: AbodeDev, event: Event) -> None: def __init__(self, data: AbodeSystem, device: Device, event: Event) -> None:
"""Initialize the Abode device.""" """Initialize the Abode device."""
AbodeDevice.__init__(self, data, device) AbodeDevice.__init__(self, data, device)
Camera.__init__(self) Camera.__init__(self)

View File

@ -1,4 +1,5 @@
"""Config flow for the Abode Security System component.""" """Config flow for the Abode Security System component."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
@ -14,16 +15,15 @@ from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED
from requests.exceptions import ConnectTimeout, HTTPError from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from .const import CONF_POLLING, DOMAIN, LOGGER from .const import CONF_POLLING, DOMAIN, LOGGER
CONF_MFA = "mfa_code" CONF_MFA = "mfa_code"
class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AbodeFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Abode.""" """Config flow for Abode."""
VERSION = 1 VERSION = 1
@ -43,7 +43,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._polling: bool = False self._polling: bool = False
self._username: str | None = None self._username: str | None = None
async def _async_abode_login(self, step_id: str) -> FlowResult: async def _async_abode_login(self, step_id: str) -> ConfigFlowResult:
"""Handle login with Abode.""" """Handle login with Abode."""
errors = {} errors = {}
@ -74,7 +74,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self._async_create_entry() return await self._async_create_entry()
async def _async_abode_mfa_login(self) -> FlowResult: async def _async_abode_mfa_login(self) -> ConfigFlowResult:
"""Handle multi-factor authentication (MFA) login with Abode.""" """Handle multi-factor authentication (MFA) login with Abode."""
try: try:
# Create instance to access login method for passing MFA code # Create instance to access login method for passing MFA code
@ -92,7 +92,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self._async_create_entry() return await self._async_create_entry()
async def _async_create_entry(self) -> FlowResult: async def _async_create_entry(self) -> ConfigFlowResult:
"""Create the config entry.""" """Create the config entry."""
config_data = { config_data = {
CONF_USERNAME: self._username, CONF_USERNAME: self._username,
@ -118,7 +118,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
@ -135,7 +135,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_mfa( async def async_step_mfa(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle a multi-factor authentication (MFA) flow.""" """Handle a multi-factor authentication (MFA) flow."""
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
@ -146,7 +146,9 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self._async_abode_mfa_login() return await self._async_abode_mfa_login()
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthorization request from Abode.""" """Handle reauthorization request from Abode."""
self._username = entry_data[CONF_USERNAME] self._username = entry_data[CONF_USERNAME]
@ -154,7 +156,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_reauth_confirm( async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle reauthorization flow.""" """Handle reauthorization flow."""
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(

View File

@ -1,4 +1,5 @@
"""Constants for the Abode Security System component.""" """Constants for the Abode Security System component."""
import logging import logging
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)

View File

@ -1,8 +1,9 @@
"""Support for Abode Security System covers.""" """Support for Abode Security System covers."""
from typing import Any from typing import Any
from jaraco.abode.devices.cover import Cover as AbodeCV from jaraco.abode.devices.cover import Cover
from jaraco.abode.helpers import constants as CONST from jaraco.abode.helpers.constants import TYPE_COVER
from homeassistant.components.cover import CoverEntity from homeassistant.components.cover import CoverEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -21,14 +22,14 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
AbodeCover(data, device) AbodeCover(data, device)
for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER) for device in data.abode.get_devices(generic_type=TYPE_COVER)
) )
class AbodeCover(AbodeDevice, CoverEntity): class AbodeCover(AbodeDevice, CoverEntity):
"""Representation of an Abode cover.""" """Representation of an Abode cover."""
_device: AbodeCV _device: Cover
_attr_name = None _attr_name = None
@property @property

View File

@ -5,5 +5,10 @@
"default": "mdi:robot" "default": "mdi:robot"
} }
} }
},
"services": {
"capture_image": "mdi:camera",
"change_setting": "mdi:cog",
"trigger_automation": "mdi:play"
} }
} }

View File

@ -1,11 +1,12 @@
"""Support for Abode Security System lights.""" """Support for Abode Security System lights."""
from __future__ import annotations from __future__ import annotations
from math import ceil from math import ceil
from typing import Any from typing import Any
from jaraco.abode.devices.light import Light as AbodeLT from jaraco.abode.devices.light import Light
from jaraco.abode.helpers import constants as CONST from jaraco.abode.helpers.constants import TYPE_LIGHT
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -34,14 +35,14 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
AbodeLight(data, device) AbodeLight(data, device)
for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT) for device in data.abode.get_devices(generic_type=TYPE_LIGHT)
) )
class AbodeLight(AbodeDevice, LightEntity): class AbodeLight(AbodeDevice, LightEntity):
"""Representation of an Abode light.""" """Representation of an Abode light."""
_device: AbodeLT _device: Light
_attr_name = None _attr_name = None
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:

View File

@ -1,8 +1,9 @@
"""Support for the Abode Security System locks.""" """Support for the Abode Security System locks."""
from typing import Any from typing import Any
from jaraco.abode.devices.lock import Lock as AbodeLK from jaraco.abode.devices.lock import Lock
from jaraco.abode.helpers import constants as CONST from jaraco.abode.helpers.constants import TYPE_LOCK
from homeassistant.components.lock import LockEntity from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -21,14 +22,14 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
AbodeLock(data, device) AbodeLock(data, device)
for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK) for device in data.abode.get_devices(generic_type=TYPE_LOCK)
) )
class AbodeLock(AbodeDevice, LockEntity): class AbodeLock(AbodeDevice, LockEntity):
"""Representation of an Abode lock.""" """Representation of an Abode lock."""
_device: AbodeLK _device: Lock
_attr_name = None _attr_name = None
def lock(self, **kwargs: Any) -> None: def lock(self, **kwargs: Any) -> None:

View File

@ -1,12 +1,21 @@
"""Support for Abode Security System sensors.""" """Support for Abode Security System sensors."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import cast from typing import cast
from jaraco.abode.devices.sensor import Sensor as AbodeSense from jaraco.abode.devices.sensor import Sensor
from jaraco.abode.helpers import constants as CONST from jaraco.abode.helpers.constants import (
HUMI_STATUS_KEY,
LUX_STATUS_KEY,
STATUSES_KEY,
TEMP_STATUS_KEY,
TYPE_SENSOR,
UNIT_CELSIUS,
UNIT_FAHRENHEIT,
)
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -22,27 +31,22 @@ from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
ABODE_TEMPERATURE_UNIT_HA_UNIT = { ABODE_TEMPERATURE_UNIT_HA_UNIT = {
CONST.UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
CONST.UNIT_CELSIUS: UnitOfTemperature.CELSIUS, UNIT_CELSIUS: UnitOfTemperature.CELSIUS,
} }
@dataclass(frozen=True) @dataclass(frozen=True, kw_only=True)
class AbodeSensorDescriptionMixin: class AbodeSensorDescription(SensorEntityDescription):
"""Mixin for Abode sensor."""
value_fn: Callable[[AbodeSense], float]
native_unit_of_measurement_fn: Callable[[AbodeSense], str]
@dataclass(frozen=True)
class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin):
"""Class describing Abode sensor entities.""" """Class describing Abode sensor entities."""
value_fn: Callable[[Sensor], float]
native_unit_of_measurement_fn: Callable[[Sensor], str]
SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
AbodeSensorDescription( AbodeSensorDescription(
key=CONST.TEMP_STATUS_KEY, key=TEMP_STATUS_KEY,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[ native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
device.temp_unit device.temp_unit
@ -50,13 +54,13 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
value_fn=lambda device: cast(float, device.temp), value_fn=lambda device: cast(float, device.temp),
), ),
AbodeSensorDescription( AbodeSensorDescription(
key=CONST.HUMI_STATUS_KEY, key=HUMI_STATUS_KEY,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement_fn=lambda _: PERCENTAGE, native_unit_of_measurement_fn=lambda _: PERCENTAGE,
value_fn=lambda device: cast(float, device.humidity), value_fn=lambda device: cast(float, device.humidity),
), ),
AbodeSensorDescription( AbodeSensorDescription(
key=CONST.LUX_STATUS_KEY, key=LUX_STATUS_KEY,
device_class=SensorDeviceClass.ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement_fn=lambda _: LIGHT_LUX, native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
value_fn=lambda device: cast(float, device.lux), value_fn=lambda device: cast(float, device.lux),
@ -73,8 +77,8 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
AbodeSensor(data, device, description) AbodeSensor(data, device, description)
for description in SENSOR_TYPES for description in SENSOR_TYPES
for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR) for device in data.abode.get_devices(generic_type=TYPE_SENSOR)
if description.key in device.get_value(CONST.STATUSES_KEY) if description.key in device.get_value(STATUSES_KEY)
) )
@ -82,12 +86,12 @@ class AbodeSensor(AbodeDevice, SensorEntity):
"""A sensor implementation for Abode devices.""" """A sensor implementation for Abode devices."""
entity_description: AbodeSensorDescription entity_description: AbodeSensorDescription
_device: AbodeSense _device: Sensor
def __init__( def __init__(
self, self,
data: AbodeSystem, data: AbodeSystem,
device: AbodeSense, device: Sensor,
description: AbodeSensorDescription, description: AbodeSensorDescription,
) -> None: ) -> None:
"""Initialize a sensor for an Abode device.""" """Initialize a sensor for an Abode device."""

View File

@ -1,10 +1,11 @@
"""Support for Abode Security System switches.""" """Support for Abode Security System switches."""
from __future__ import annotations from __future__ import annotations
from typing import Any, cast from typing import Any, cast
from jaraco.abode.devices.switch import Switch as AbodeSW from jaraco.abode.devices.switch import Switch
from jaraco.abode.helpers import constants as CONST from jaraco.abode.helpers.constants import TYPE_SWITCH, TYPE_VALVE
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -15,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeAutomation, AbodeDevice, AbodeSystem from . import AbodeAutomation, AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE] DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE]
async def async_setup_entry( async def async_setup_entry(
@ -41,7 +42,7 @@ async def async_setup_entry(
class AbodeSwitch(AbodeDevice, SwitchEntity): class AbodeSwitch(AbodeDevice, SwitchEntity):
"""Representation of an Abode switch.""" """Representation of an Abode switch."""
_device: AbodeSW _device: Switch
_attr_name = None _attr_name = None
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:

View File

@ -1,4 +1,5 @@
"""The AccuWeather component.""" """The AccuWeather component."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout from asyncio import timeout
@ -51,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Remove ozone sensors from registry if they exist # Remove ozone sensors from registry if they exist
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
for day in range(0, 5): for day in range(5):
unique_id = f"{coordinator.location_key}-ozone-{day}" unique_id = f"{coordinator.location_key}-ozone-{day}"
if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id): if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id):
_LOGGER.debug("Removing ozone sensor entity %s", entity_id) _LOGGER.debug("Removing ozone sensor entity %s", entity_id)
@ -134,4 +135,4 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) as error: ) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return {**current, **{ATTR_FORECAST: forecast}} return {**current, ATTR_FORECAST: forecast}

View File

@ -1,4 +1,5 @@
"""Adds config flow for AccuWeather.""" """Adds config flow for AccuWeather."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout from asyncio import timeout
@ -9,11 +10,9 @@ from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import ( from homeassistant.helpers.schema_config_entry_flow import (
@ -33,20 +32,15 @@ OPTIONS_FLOW = {
} }
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for AccuWeather.""" """Config flow for AccuWeather."""
VERSION = 1 VERSION = 1
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
# Under the terms of use of the API, one user can use one free API key. Due to
# the small number of requests allowed, we only allow one integration instance.
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors = {} errors = {}
if user_input is not None: if user_input is not None:

View File

@ -1,4 +1,5 @@
"""Constants for AccuWeather integration.""" """Constants for AccuWeather integration."""
from __future__ import annotations from __future__ import annotations
from typing import Final from typing import Final

View File

@ -1,4 +1,5 @@
"""Diagnostics support for AccuWeather.""" """Diagnostics support for AccuWeather."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -8,5 +8,6 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["accuweather"], "loggers": ["accuweather"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["accuweather==2.1.1"] "requirements": ["accuweather==2.1.1"],
"single_config_entry": true
} }

View File

@ -1,4 +1,5 @@
"""Support for the AccuWeather service.""" """Support for the AccuWeather service."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
@ -45,19 +46,11 @@ from .const import (
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@dataclass(frozen=True) @dataclass(frozen=True, kw_only=True)
class AccuWeatherSensorDescriptionMixin: class AccuWeatherSensorDescription(SensorEntityDescription):
"""Mixin for AccuWeather sensor."""
value_fn: Callable[[dict[str, Any]], str | int | float | None]
@dataclass(frozen=True)
class AccuWeatherSensorDescription(
SensorEntityDescription, AccuWeatherSensorDescriptionMixin
):
"""Class describing AccuWeather sensor entities.""" """Class describing AccuWeather sensor entities."""
value_fn: Callable[[dict[str, Any]], str | int | float | None]
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
day: int | None = None day: int | None = None

View File

@ -17,9 +17,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key." "requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
} }
}, },
"entity": { "entity": {

View File

@ -1,4 +1,5 @@
"""Provide info to system health.""" """Provide info to system health."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -1,4 +1,5 @@
"""Support for the AccuWeather service.""" """Support for the AccuWeather service."""
from __future__ import annotations from __future__ import annotations
from typing import cast from typing import cast
@ -145,9 +146,9 @@ class AccuWeatherEntity(
"""Return the UV index.""" """Return the UV index."""
return cast(float, self.coordinator.data["UVIndex"]) return cast(float, self.coordinator.data["UVIndex"])
@property @callback
def forecast(self) -> list[Forecast] | None: def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the forecast array.""" """Return the daily forecast in native units."""
if not self.coordinator.forecast: if not self.coordinator.forecast:
return None return None
# remap keys from library to keys understood by the weather component # remap keys from library to keys understood by the weather component
@ -176,8 +177,3 @@ class AccuWeatherEntity(
} }
for item in self.coordinator.data[ATTR_FORECAST] for item in self.coordinator.data[ATTR_FORECAST]
] ]
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast in native units."""
return self.forecast

View File

@ -1,4 +1,5 @@
"""Use serial protocol of Acer projector to obtain state of the projector.""" """Use serial protocol of Acer projector to obtain state of the projector."""
from __future__ import annotations from __future__ import annotations
from typing import Final from typing import Final

View File

@ -1,4 +1,5 @@
"""Use serial protocol of Acer projector to obtain state of the projector.""" """Use serial protocol of Acer projector to obtain state of the projector."""
from __future__ import annotations from __future__ import annotations
import logging import logging

View File

@ -1,4 +1,5 @@
"""The Rollease Acmeda Automate integration.""" """The Rollease Acmeda Automate integration."""
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

@ -1,4 +1,5 @@
"""Base class for Acmeda Roller Blinds.""" """Base class for Acmeda Roller Blinds."""
from __future__ import annotations from __future__ import annotations
import aiopulse import aiopulse

View File

@ -1,4 +1,5 @@
"""Config flow for Rollease Acmeda Automate Pulse Hub.""" """Config flow for Rollease Acmeda Automate Pulse Hub."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout from asyncio import timeout
@ -8,14 +9,13 @@ from typing import Any
import aiopulse import aiopulse
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_ID from homeassistant.const import CONF_HOST, CONF_ID
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN from .const import DOMAIN
class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Acmeda config flow.""" """Handle a Acmeda config flow."""
VERSION = 1 VERSION = 1
@ -26,7 +26,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if ( if (
user_input is not None user_input is not None
@ -40,12 +40,13 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
entry.unique_id for entry in self._async_current_entries() entry.unique_id for entry in self._async_current_entries()
} }
hubs: list[aiopulse.Hub] = []
with suppress(TimeoutError): with suppress(TimeoutError):
async with timeout(5): async with timeout(5):
async for hub in aiopulse.Hub.discover(): hubs: list[aiopulse.Hub] = [
if hub.id not in already_configured: hub
hubs.append(hub) async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured
]
if not hubs: if not hubs:
return self.async_abort(reason="no_devices_found") return self.async_abort(reason="no_devices_found")
@ -66,7 +67,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
), ),
) )
async def async_create(self, hub: aiopulse.Hub) -> FlowResult: async def async_create(self, hub: aiopulse.Hub) -> ConfigFlowResult:
"""Create the Acmeda Hub entry.""" """Create the Acmeda Hub entry."""
await self.async_set_unique_id(hub.id, raise_on_progress=False) await self.async_set_unique_id(hub.id, raise_on_progress=False)
return self.async_create_entry(title=hub.id, data={CONF_HOST: hub.host}) return self.async_create_entry(title=hub.id, data={CONF_HOST: hub.host})

View File

@ -1,4 +1,5 @@
"""Constants for the Rollease Acmeda Automate integration.""" """Constants for the Rollease Acmeda Automate integration."""
import logging import logging
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)

View File

@ -1,4 +1,5 @@
"""Support for Acmeda Roller Blinds.""" """Support for Acmeda Roller Blinds."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -1,4 +1,5 @@
"""Errors for the Acmeda Pulse component.""" """Errors for the Acmeda Pulse component."""
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError

View File

@ -1,4 +1,5 @@
"""Helper functions for Acmeda Pulse.""" """Helper functions for Acmeda Pulse."""
from __future__ import annotations from __future__ import annotations
from aiopulse import Roller from aiopulse import Roller

View File

@ -1,4 +1,5 @@
"""Code to handle a Pulse Hub.""" """Code to handle a Pulse Hub."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio

View File

@ -1,4 +1,5 @@
"""Support for Acmeda Roller Blind Batteries.""" """Support for Acmeda Roller Blind Batteries."""
from __future__ import annotations from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.components.sensor import SensorDeviceClass, SensorEntity

View File

@ -1,4 +1,5 @@
"""Support for Actiontec MI424WR (Verizon FIOS) routers.""" """Support for Actiontec MI424WR (Verizon FIOS) routers."""
from __future__ import annotations from __future__ import annotations
import re import re
@ -8,7 +9,7 @@ from typing import Final
LEASES_REGEX: Final[re.Pattern[str]] = re.compile( LEASES_REGEX: Final[re.Pattern[str]] = re.compile(
r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})" r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})"
+ r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
+ r"\svalid\sfor:\s(?P<timevalid>(-?\d+))" r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
+ r"\ssec" r"\ssec"
) )

View File

@ -1,4 +1,5 @@
"""Support for Actiontec MI424WR (Verizon FIOS) routers.""" """Support for Actiontec MI424WR (Verizon FIOS) routers."""
from __future__ import annotations from __future__ import annotations
import logging import logging

View File

@ -1,4 +1,5 @@
"""Model definitions for Actiontec MI424WR (Verizon FIOS) routers.""" """Model definitions for Actiontec MI424WR (Verizon FIOS) routers."""
from dataclasses import dataclass from dataclasses import dataclass

View File

@ -1,4 +1,5 @@
"""The Adax integration.""" """The Adax integration."""
from __future__ import annotations from __future__ import annotations
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry

View File

@ -1,4 +1,5 @@
"""Support for Adax wifi-enabled home heaters.""" """Support for Adax wifi-enabled home heaters."""
from __future__ import annotations from __future__ import annotations
from typing import Any, cast from typing import Any, cast

View File

@ -1,4 +1,5 @@
"""Config flow for Adax integration.""" """Config flow for Adax integration."""
from __future__ import annotations from __future__ import annotations
import logging import logging
@ -8,14 +9,13 @@ import adax
import adax_local import adax_local
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import ( from homeassistant.const import (
CONF_IP_ADDRESS, CONF_IP_ADDRESS,
CONF_PASSWORD, CONF_PASSWORD,
CONF_TOKEN, CONF_TOKEN,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
) )
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ( from .const import (
@ -31,14 +31,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Adax.""" """Handle a config flow for Adax."""
VERSION = 2 VERSION = 2
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle the initial step.""" """Handle the initial step."""
data_schema = vol.Schema( data_schema = vol.Schema(
{ {
@ -63,7 +63,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_local( async def async_step_local(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle the local step.""" """Handle the local step."""
data_schema = vol.Schema( data_schema = vol.Schema(
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str} {vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}
@ -110,7 +110,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_cloud( async def async_step_cloud(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle the cloud step.""" """Handle the cloud step."""
data_schema = vol.Schema( data_schema = vol.Schema(
{vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str} {vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str}

View File

@ -1,4 +1,5 @@
"""Constants for the Adax integration.""" """Constants for the Adax integration."""
from typing import Final from typing import Final
ACCOUNT_ID: Final = "account_id" ACCOUNT_ID: Final = "account_id"

View File

@ -1,4 +1,5 @@
"""Support for AdGuard Home.""" """Support for AdGuard Home."""
from __future__ import annotations from __future__ import annotations
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError

View File

@ -1,4 +1,5 @@
"""Config flow to configure the AdGuard Home integration.""" """Config flow to configure the AdGuard Home integration."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@ -7,7 +8,7 @@ from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
@ -16,7 +17,6 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
@ -31,7 +31,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
async def _show_setup_form( async def _show_setup_form(
self, errors: dict[str, str] | None = None self, errors: dict[str, str] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Show the setup form to the user.""" """Show the setup form to the user."""
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
@ -50,7 +50,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
async def _show_hassio_form( async def _show_hassio_form(
self, errors: dict[str, str] | None = None self, errors: dict[str, str] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Show the Hass.io confirmation form to the user.""" """Show the Hass.io confirmation form to the user."""
assert self._hassio_discovery assert self._hassio_discovery
return self.async_show_form( return self.async_show_form(
@ -61,7 +61,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
if user_input is None: if user_input is None:
return await self._show_setup_form(user_input) return await self._show_setup_form(user_input)
@ -104,7 +104,9 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
}, },
) )
async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: async def async_step_hassio(
self, discovery_info: HassioServiceInfo
) -> ConfigFlowResult:
"""Prepare configuration for a Hass.io AdGuard Home add-on. """Prepare configuration for a Hass.io AdGuard Home add-on.
This flow is triggered by the discovery component. This flow is triggered by the discovery component.
@ -116,7 +118,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_hassio_confirm( async def async_step_hassio_confirm(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> ConfigFlowResult:
"""Confirm Supervisor discovery.""" """Confirm Supervisor discovery."""
if user_input is None: if user_input is None:
return await self._show_hassio_form() return await self._show_hassio_form()

View File

@ -1,4 +1,5 @@
"""Constants for the AdGuard Home integration.""" """Constants for the AdGuard Home integration."""
import logging import logging
DOMAIN = "adguard" DOMAIN = "adguard"

View File

@ -1,4 +1,5 @@
"""AdGuard Home base entity.""" """AdGuard Home base entity."""
from __future__ import annotations from __future__ import annotations
from adguardhome import AdGuardHome, AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeError
@ -43,7 +44,7 @@ class AdGuardHomeEntity(Entity):
async def _adguard_update(self) -> None: async def _adguard_update(self) -> None:
"""Update AdGuard Home entity.""" """Update AdGuard Home entity."""
raise NotImplementedError() raise NotImplementedError
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:

View File

@ -1,4 +1,5 @@
"""Support for AdGuard Home sensors.""" """Support for AdGuard Home sensors."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine

View File

@ -1,4 +1,5 @@
"""Support for AdGuard Home switches.""" """Support for AdGuard Home switches."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine

View File

@ -1,4 +1,5 @@
"""Support for Automation Device Specification (ADS).""" """Support for Automation Device Specification (ADS)."""
import asyncio import asyncio
from asyncio import timeout from asyncio import timeout
from collections import namedtuple from collections import namedtuple

View File

@ -1,4 +1,5 @@
"""Support for ADS binary sensors.""" """Support for ADS binary sensors."""
from __future__ import annotations from __future__ import annotations
import pyads import pyads

View File

@ -1,4 +1,5 @@
"""Support for ADS covers.""" """Support for ADS covers."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -0,0 +1,5 @@
{
"services": {
"write_data_by_name": "mdi:pencil"
}
}

View File

@ -1,4 +1,5 @@
"""Support for ADS light sources.""" """Support for ADS light sources."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -1,4 +1,5 @@
"""Support for ADS sensors.""" """Support for ADS sensors."""
from __future__ import annotations from __future__ import annotations
import voluptuous as vol import voluptuous as vol

View File

@ -1,4 +1,5 @@
"""Support for ADS switch platform.""" """Support for ADS switch platform."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any

View File

@ -1,4 +1,5 @@
"""Advantage Air climate integration.""" """Advantage Air climate integration."""
from datetime import timedelta from datetime import timedelta
import logging import logging

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