Merge pull request #74522 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2022-07-06 20:32:56 +02:00 committed by GitHub
commit eb0f8f9542
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2289 changed files with 53625 additions and 26109 deletions

View File

@ -132,7 +132,7 @@ requirements: &requirements
- homeassistant/package_constraints.txt - homeassistant/package_constraints.txt
- script/pip_check - script/pip_check
- requirements*.txt - requirements*.txt
- setup.cfg - pyproject.toml
any: any:
- *base_platforms - *base_platforms

View File

@ -210,7 +210,6 @@ omit =
homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/media_player.py
homeassistant/components/denonavr/receiver.py homeassistant/components/denonavr/receiver.py
homeassistant/components/deutsche_bahn/sensor.py homeassistant/components/deutsche_bahn/sensor.py
homeassistant/components/devolo_home_control/cover.py
homeassistant/components/devolo_home_control/light.py homeassistant/components/devolo_home_control/light.py
homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/sensor.py
homeassistant/components/devolo_home_control/switch.py homeassistant/components/devolo_home_control/switch.py
@ -262,7 +261,9 @@ omit =
homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/eddystone_temperature/sensor.py
homeassistant/components/edimax/switch.py homeassistant/components/edimax/switch.py
homeassistant/components/egardia/* homeassistant/components/egardia/*
homeassistant/components/eight_sleep/* homeassistant/components/eight_sleep/__init__.py
homeassistant/components/eight_sleep/binary_sensor.py
homeassistant/components/eight_sleep/sensor.py
homeassistant/components/eliqonline/sensor.py homeassistant/components/eliqonline/sensor.py
homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/__init__.py
homeassistant/components/elkm1/alarm_control_panel.py homeassistant/components/elkm1/alarm_control_panel.py
@ -407,6 +408,7 @@ omit =
homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/const.py
homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/base.py
homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_callmonitor/sensor.py
homeassistant/components/frontier_silicon/const.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/garadget/cover.py homeassistant/components/garadget/cover.py
@ -504,16 +506,21 @@ omit =
homeassistant/components/huawei_lte/switch.py homeassistant/components/huawei_lte/switch.py
homeassistant/components/hue/light.py homeassistant/components/hue/light.py
homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/__init__.py
homeassistant/components/hunterdouglas_powerview/button.py
homeassistant/components/hunterdouglas_powerview/coordinator.py
homeassistant/components/hunterdouglas_powerview/cover.py
homeassistant/components/hunterdouglas_powerview/diagnostics.py
homeassistant/components/hunterdouglas_powerview/entity.py
homeassistant/components/hunterdouglas_powerview/model.py
homeassistant/components/hunterdouglas_powerview/scene.py homeassistant/components/hunterdouglas_powerview/scene.py
homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/sensor.py
homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/shade_data.py
homeassistant/components/hunterdouglas_powerview/entity.py homeassistant/components/hunterdouglas_powerview/util.py
homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/binary_sensor.py
homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/sensor.py
homeassistant/components/hvv_departures/__init__.py homeassistant/components/hvv_departures/__init__.py
homeassistant/components/hydrawise/* homeassistant/components/hydrawise/*
homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/ialarm/alarm_control_panel.py
homeassistant/components/ialarm_xr/alarm_control_panel.py
homeassistant/components/iammeter/sensor.py homeassistant/components/iammeter/sensor.py
homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/binary_sensor.py
homeassistant/components/iaqualink/climate.py homeassistant/components/iaqualink/climate.py
@ -575,6 +582,7 @@ omit =
homeassistant/components/isy994/sensor.py homeassistant/components/isy994/sensor.py
homeassistant/components/isy994/services.py homeassistant/components/isy994/services.py
homeassistant/components/isy994/switch.py homeassistant/components/isy994/switch.py
homeassistant/components/isy994/util.py
homeassistant/components/itach/remote.py homeassistant/components/itach/remote.py
homeassistant/components/itunes/media_player.py homeassistant/components/itunes/media_player.py
homeassistant/components/jellyfin/__init__.py homeassistant/components/jellyfin/__init__.py
@ -624,18 +632,16 @@ omit =
homeassistant/components/launch_library/const.py homeassistant/components/launch_library/const.py
homeassistant/components/launch_library/diagnostics.py homeassistant/components/launch_library/diagnostics.py
homeassistant/components/launch_library/sensor.py homeassistant/components/launch_library/sensor.py
homeassistant/components/lcn/binary_sensor.py
homeassistant/components/lcn/climate.py homeassistant/components/lcn/climate.py
homeassistant/components/lcn/helpers.py homeassistant/components/lcn/helpers.py
homeassistant/components/lcn/scene.py homeassistant/components/lcn/scene.py
homeassistant/components/lcn/sensor.py
homeassistant/components/lcn/services.py homeassistant/components/lcn/services.py
homeassistant/components/lg_netcast/media_player.py homeassistant/components/lg_netcast/media_player.py
homeassistant/components/lg_soundbar/media_player.py homeassistant/components/lg_soundbar/media_player.py
homeassistant/components/life360/__init__.py homeassistant/components/life360/__init__.py
homeassistant/components/life360/const.py homeassistant/components/life360/const.py
homeassistant/components/life360/coordinator.py
homeassistant/components/life360/device_tracker.py homeassistant/components/life360/device_tracker.py
homeassistant/components/life360/helpers.py
homeassistant/components/lifx/__init__.py homeassistant/components/lifx/__init__.py
homeassistant/components/lifx/const.py homeassistant/components/lifx/const.py
homeassistant/components/lifx/light.py homeassistant/components/lifx/light.py
@ -671,6 +677,7 @@ omit =
homeassistant/components/lutron_caseta/light.py homeassistant/components/lutron_caseta/light.py
homeassistant/components/lutron_caseta/scene.py homeassistant/components/lutron_caseta/scene.py
homeassistant/components/lutron_caseta/switch.py homeassistant/components/lutron_caseta/switch.py
homeassistant/components/lutron_caseta/util.py
homeassistant/components/lw12wifi/light.py homeassistant/components/lw12wifi/light.py
homeassistant/components/lyric/__init__.py homeassistant/components/lyric/__init__.py
homeassistant/components/lyric/api.py homeassistant/components/lyric/api.py
@ -785,6 +792,7 @@ omit =
homeassistant/components/netgear/router.py homeassistant/components/netgear/router.py
homeassistant/components/netgear/sensor.py homeassistant/components/netgear/sensor.py
homeassistant/components/netgear/switch.py homeassistant/components/netgear/switch.py
homeassistant/components/netgear/update.py
homeassistant/components/netgear_lte/* homeassistant/components/netgear_lte/*
homeassistant/components/netio/switch.py homeassistant/components/netio/switch.py
homeassistant/components/neurio_energy/sensor.py homeassistant/components/neurio_energy/sensor.py
@ -957,7 +965,13 @@ omit =
homeassistant/components/radarr/sensor.py homeassistant/components/radarr/sensor.py
homeassistant/components/radio_browser/__init__.py homeassistant/components/radio_browser/__init__.py
homeassistant/components/radio_browser/media_source.py homeassistant/components/radio_browser/media_source.py
homeassistant/components/radiotherm/__init__.py
homeassistant/components/radiotherm/entity.py
homeassistant/components/radiotherm/climate.py homeassistant/components/radiotherm/climate.py
homeassistant/components/radiotherm/coordinator.py
homeassistant/components/radiotherm/data.py
homeassistant/components/radiotherm/switch.py
homeassistant/components/radiotherm/util.py
homeassistant/components/rainbird/* homeassistant/components/rainbird/*
homeassistant/components/raincloud/* homeassistant/components/raincloud/*
homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/__init__.py
@ -1049,6 +1063,7 @@ omit =
homeassistant/components/shelly/sensor.py homeassistant/components/shelly/sensor.py
homeassistant/components/shelly/utils.py homeassistant/components/shelly/utils.py
homeassistant/components/sigfox/sensor.py homeassistant/components/sigfox/sensor.py
homeassistant/components/simplepush/__init__.py
homeassistant/components/simplepush/notify.py homeassistant/components/simplepush/notify.py
homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/__init__.py
homeassistant/components/simplisafe/alarm_control_panel.py homeassistant/components/simplisafe/alarm_control_panel.py
@ -1059,7 +1074,14 @@ omit =
homeassistant/components/sisyphus/* homeassistant/components/sisyphus/*
homeassistant/components/sky_hub/* homeassistant/components/sky_hub/*
homeassistant/components/skybeacon/sensor.py homeassistant/components/skybeacon/sensor.py
homeassistant/components/skybell/* homeassistant/components/skybell/__init__.py
homeassistant/components/skybell/binary_sensor.py
homeassistant/components/skybell/camera.py
homeassistant/components/skybell/coordinator.py
homeassistant/components/skybell/entity.py
homeassistant/components/skybell/light.py
homeassistant/components/skybell/sensor.py
homeassistant/components/skybell/switch.py
homeassistant/components/slack/__init__.py homeassistant/components/slack/__init__.py
homeassistant/components/slack/notify.py homeassistant/components/slack/notify.py
homeassistant/components/sia/__init__.py homeassistant/components/sia/__init__.py
@ -1100,12 +1122,6 @@ omit =
homeassistant/components/soma/cover.py homeassistant/components/soma/cover.py
homeassistant/components/soma/sensor.py homeassistant/components/soma/sensor.py
homeassistant/components/soma/utils.py homeassistant/components/soma/utils.py
homeassistant/components/somfy/__init__.py
homeassistant/components/somfy/api.py
homeassistant/components/somfy/climate.py
homeassistant/components/somfy/cover.py
homeassistant/components/somfy/sensor.py
homeassistant/components/somfy/switch.py
homeassistant/components/somfy_mylink/__init__.py homeassistant/components/somfy_mylink/__init__.py
homeassistant/components/somfy_mylink/cover.py homeassistant/components/somfy_mylink/cover.py
homeassistant/components/sonos/__init__.py homeassistant/components/sonos/__init__.py
@ -1181,6 +1197,7 @@ omit =
homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/binary_sensor.py
homeassistant/components/synology_dsm/button.py homeassistant/components/synology_dsm/button.py
homeassistant/components/synology_dsm/camera.py homeassistant/components/synology_dsm/camera.py
homeassistant/components/synology_dsm/coordinator.py
homeassistant/components/synology_dsm/diagnostics.py homeassistant/components/synology_dsm/diagnostics.py
homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/common.py
homeassistant/components/synology_dsm/entity.py homeassistant/components/synology_dsm/entity.py
@ -1284,9 +1301,6 @@ omit =
homeassistant/components/tradfri/light.py homeassistant/components/tradfri/light.py
homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/sensor.py
homeassistant/components/tradfri/switch.py homeassistant/components/tradfri/switch.py
homeassistant/components/trafikverket_ferry/__init__.py
homeassistant/components/trafikverket_ferry/coordinator.py
homeassistant/components/trafikverket_ferry/sensor.py
homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/__init__.py
homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/__init__.py
@ -1345,6 +1359,7 @@ omit =
homeassistant/components/vasttrafik/sensor.py homeassistant/components/vasttrafik/sensor.py
homeassistant/components/velbus/__init__.py homeassistant/components/velbus/__init__.py
homeassistant/components/velbus/binary_sensor.py homeassistant/components/velbus/binary_sensor.py
homeassistant/components/velbus/button.py
homeassistant/components/velbus/climate.py homeassistant/components/velbus/climate.py
homeassistant/components/velbus/const.py homeassistant/components/velbus/const.py
homeassistant/components/velbus/cover.py homeassistant/components/velbus/cover.py
@ -1476,9 +1491,11 @@ omit =
homeassistant/components/yolink/__init__.py homeassistant/components/yolink/__init__.py
homeassistant/components/yolink/api.py homeassistant/components/yolink/api.py
homeassistant/components/yolink/binary_sensor.py homeassistant/components/yolink/binary_sensor.py
homeassistant/components/yolink/climate.py
homeassistant/components/yolink/const.py homeassistant/components/yolink/const.py
homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/coordinator.py
homeassistant/components/yolink/entity.py homeassistant/components/yolink/entity.py
homeassistant/components/yolink/lock.py
homeassistant/components/yolink/sensor.py homeassistant/components/yolink/sensor.py
homeassistant/components/yolink/siren.py homeassistant/components/yolink/siren.py
homeassistant/components/yolink/switch.py homeassistant/components/yolink/switch.py
@ -1500,7 +1517,6 @@ omit =
homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/gateway.py
homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/helpers.py
homeassistant/components/zha/core/registries.py homeassistant/components/zha/core/registries.py
homeassistant/components/zha/core/typing.py
homeassistant/components/zha/entity.py homeassistant/components/zha/entity.py
homeassistant/components/zha/light.py homeassistant/components/zha/light.py
homeassistant/components/zha/sensor.py homeassistant/components/zha/sensor.py

View File

@ -29,7 +29,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -70,7 +70,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -104,7 +104,7 @@ 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@v3.1.2 uses: actions/setup-python@v4.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -135,7 +135,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2022.03.1 uses: home-assistant/builder@2022.06.2
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -171,6 +171,7 @@ jobs:
- raspberrypi4 - raspberrypi4
- raspberrypi4-64 - raspberrypi4-64
- tinker - tinker
- yellow
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
@ -200,7 +201,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2022.03.1 uses: home-assistant/builder@2022.06.2
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \

View File

@ -20,14 +20,15 @@ on:
type: boolean type: boolean
env: env:
CACHE_VERSION: 9 CACHE_VERSION: 10
PIP_CACHE_VERSION: 3 PIP_CACHE_VERSION: 4
HA_SHORT_VERSION: 2022.6 HA_SHORT_VERSION: 2022.7
DEFAULT_PYTHON: 3.9 DEFAULT_PYTHON: 3.9
PRE_COMMIT_CACHE: ~/.cache/pre-commit PRE_COMMIT_CACHE: ~/.cache/pre-commit
PIP_CACHE: /tmp/pip-cache PIP_CACHE: /tmp/pip-cache
SQLALCHEMY_WARN_20: 1 SQLALCHEMY_WARN_20: 1
PYTHONASYNCIODEBUG: 1 PYTHONASYNCIODEBUG: 1
HASS_CI: 1
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@ -155,7 +156,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
@ -172,7 +173,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')"
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: >- key: >-
@ -189,7 +190,7 @@ jobs:
# ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
- name: Restore pip wheel cache - name: Restore pip wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: ${{ env.PIP_CACHE }} path: ${{ env.PIP_CACHE }}
key: >- key: >-
@ -212,7 +213,7 @@ jobs:
hashFiles('.pre-commit-config.yaml') }}" hashFiles('.pre-commit-config.yaml') }}"
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: >- key: >-
@ -235,13 +236,13 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@ -253,7 +254,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@ -285,13 +286,13 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@ -303,7 +304,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@ -336,13 +337,13 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@ -354,7 +355,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@ -378,13 +379,13 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@ -396,7 +397,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
@ -502,7 +503,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- 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@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@ -525,13 +526,13 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
@ -573,7 +574,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')"
- 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@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: >- key: >-
@ -590,7 +591,7 @@ jobs:
# ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
- name: Restore pip wheel cache - name: Restore pip wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: ${{ env.PIP_CACHE }} path: ${{ env.PIP_CACHE }}
key: >- key: >-
@ -629,7 +630,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- 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@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@ -671,7 +672,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- 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@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@ -689,7 +690,7 @@ jobs:
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
mypy homeassistant mypy homeassistant pylint
- name: Run mypy (partially) - name: Run mypy (partially)
if: needs.changes.outputs.test_full_suite == 'false' if: needs.changes.outputs.test_full_suite == 'false'
shell: bash shell: bash
@ -715,7 +716,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- 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@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@ -758,7 +759,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- 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@v3.0.2 uses: actions/cache@v3.0.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ matrix.python-version }}-${{ key: ${{ runner.os }}-${{ matrix.python-version }}-${{
@ -795,14 +796,14 @@ jobs:
--dist=loadfile \ --dist=loadfile \
--test-group-count ${{ needs.changes.outputs.test_group_count }} \ --test-group-count ${{ needs.changes.outputs.test_group_count }} \
--test-group=${{ matrix.group }} \ --test-group=${{ matrix.group }} \
--cov homeassistant \ --cov="homeassistant" \
--cov-report=xml \ --cov-report=xml \
-o console_output_style=count \ -o console_output_style=count \
-p no:sugar \ -p no:sugar \
tests tests
- name: Run pytest (partially) - name: Run pytest (partially)
if: needs.changes.outputs.test_full_suite == 'false' if: needs.changes.outputs.test_full_suite == 'false'
timeout-minutes: 10 timeout-minutes: 20
shell: bash shell: bash
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -818,7 +819,7 @@ jobs:
--timeout=9 \ --timeout=9 \
--durations=10 \ --durations=10 \
-n auto \ -n auto \
--cov homeassistant.components.${{ matrix.group }} \ --cov="homeassistant.components.${{ matrix.group }}" \
--cov-report=xml \ --cov-report=xml \
--cov-report=term-missing \ --cov-report=term-missing \
-o console_output_style=count \ -o console_output_style=count \

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v3.1.2 uses: actions/setup-python@v4.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -47,6 +47,13 @@ jobs:
# execinfo-dev when building wheels. The setuptools build setup does not have an option for # execinfo-dev when building wheels. The setuptools build setup does not have an option for
# adding a single LDFLAG so copy all relevant linux flags here (as of 1.43.0) # adding a single LDFLAG so copy all relevant linux flags here (as of 1.43.0)
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc -lexecinfo" echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc -lexecinfo"
# Fix out of memory issues with rust
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
# OpenCV headless installation
echo "CI_BUILD=1"
echo "ENABLE_HEADLESS=1"
) > .env_file ) > .env_file
- name: Upload env_file - name: Upload env_file
@ -62,7 +69,7 @@ jobs:
path: ./requirements_diff.txt path: ./requirements_diff.txt
core: core:
name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for core name: Build musllinux wheels with musllinux_1_2 / cp310 at ${{ matrix.arch }} for core
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
needs: init needs: init
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -70,8 +77,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
tag:
- "3.9-alpine3.14"
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
@ -87,23 +92,21 @@ jobs:
name: requirements_diff name: requirements_diff
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2022.01.2 uses: home-assistant/wheels@2022.06.7
with: with:
tag: ${{ matrix.tag }} abi: cp310
tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-host: wheels.hass.io
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-user: wheels
env-file: true env-file: true
apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;cargo" apk: "libffi-dev;openssl-dev;yaml-dev"
pip: "Cython;numpy==1.21.6"
skip-binary: aiohttp skip-binary: aiohttp
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt" requirements: "requirements.txt"
integrations: integrations:
name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for integrations name: Build musllinux wheels with musllinux_1_2 / cp310 at ${{ matrix.arch }} for integrations
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
needs: init needs: init
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -111,8 +114,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
tag:
- "3.9-alpine3.14"
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.0.2 uses: actions/checkout@v3.0.2
@ -132,35 +133,41 @@ jobs:
requirement_files="requirements_all.txt requirements_diff.txt" requirement_files="requirements_all.txt requirements_diff.txt"
for requirement_file in ${requirement_files}; do for requirement_file in ${requirement_files}; do
sed -i "s|# pybluez|pybluez|g" ${requirement_file} sed -i "s|# pybluez|pybluez|g" ${requirement_file}
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
sed -i "s|# beacontools|beacontools|g" ${requirement_file} sed -i "s|# beacontools|beacontools|g" ${requirement_file}
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
sed -i "s|# evdev|evdev|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file}
sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file}
sed -i "s|# pycups|pycups|g" ${requirement_file} sed -i "s|# pycups|pycups|g" ${requirement_file}
sed -i "s|# homekit|homekit|g" ${requirement_file} sed -i "s|# homekit|homekit|g" ${requirement_file}
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
sed -i "s|# decora|decora|g" ${requirement_file}
sed -i "s|# avion|avion|g" ${requirement_file}
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file}
done done
- name: Adjust build env
run: |
if [ "${{ matrix.arch }}" = "i386" ]; then
echo "NPY_DISABLE_SVML=1" >> .env_file
fi
(
# cmake > 3.22.2 have issue on arm
# Tested until 3.22.5
echo "cmake==3.22.2"
) >> homeassistant/package_constraints.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2022.01.2 uses: home-assistant/wheels@2022.06.7
with: with:
tag: ${{ matrix.tag }} abi: cp310
tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-host: wheels.hass.io
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
wheels-user: wheels
env-file: true env-file: true
apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;cargo" apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev"
pip: "Cython;numpy;scikit-build" skip-binary: aiohttp;grpcio
skip-binary: aiohttp,grpcio legacy: true
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txt" requirements: "requirements_all.txt"

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.32.1 rev: v2.34.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py39-plus]
@ -93,7 +93,7 @@ repos:
language: script language: script
types: [python] types: [python]
require_serial: true require_serial: true
files: ^homeassistant/.+\.py$ files: ^(homeassistant|pylint)/.+\.py$
- id: pylint - id: pylint
name: pylint name: pylint
entry: script/run-in-env.sh pylint -j 0 entry: script/run-in-env.sh pylint -j 0
@ -106,7 +106,7 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]
files: ^(homeassistant/.+/manifest\.json|setup\.cfg|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ files: ^(homeassistant/.+/manifest\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
- id: hassfest - id: hassfest
name: hassfest name: hassfest
entry: script/run-in-env.sh python3 -m script.hassfest entry: script/run-in-env.sh python3 -m script.hassfest
@ -120,7 +120,7 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|setup\.cfg)$ files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$
- id: hassfest-mypy-config - id: hassfest-mypy-config
name: hassfest-mypy-config name: hassfest-mypy-config
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config

View File

@ -83,9 +83,11 @@ homeassistant.components.dunehd.*
homeassistant.components.efergy.* homeassistant.components.efergy.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.elkm1.* homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
homeassistant.components.esphome.* homeassistant.components.esphome.*
homeassistant.components.energy.* homeassistant.components.energy.*
homeassistant.components.evil_genius_labs.* homeassistant.components.evil_genius_labs.*
homeassistant.components.fan.*
homeassistant.components.fastdotcom.* homeassistant.components.fastdotcom.*
homeassistant.components.filesize.* homeassistant.components.filesize.*
homeassistant.components.fitbit.* homeassistant.components.fitbit.*
@ -113,6 +115,7 @@ homeassistant.components.homekit.aidmanager
homeassistant.components.homekit.config_flow homeassistant.components.homekit.config_flow
homeassistant.components.homekit.diagnostics homeassistant.components.homekit.diagnostics
homeassistant.components.homekit.logbook homeassistant.components.homekit.logbook
homeassistant.components.homekit.type_locks
homeassistant.components.homekit.type_triggers homeassistant.components.homekit.type_triggers
homeassistant.components.homekit.util homeassistant.components.homekit.util
homeassistant.components.homekit_controller homeassistant.components.homekit_controller
@ -127,7 +130,6 @@ homeassistant.components.homewizard.*
homeassistant.components.http.* homeassistant.components.http.*
homeassistant.components.huawei_lte.* homeassistant.components.huawei_lte.*
homeassistant.components.hyperion.* homeassistant.components.hyperion.*
homeassistant.components.ialarm_xr.*
homeassistant.components.image_processing.* homeassistant.components.image_processing.*
homeassistant.components.input_button.* homeassistant.components.input_button.*
homeassistant.components.input_select.* homeassistant.components.input_select.*
@ -196,6 +198,7 @@ homeassistant.components.rtsp_to_webrtc.*
homeassistant.components.samsungtv.* homeassistant.components.samsungtv.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.select.* homeassistant.components.select.*
homeassistant.components.sensibo.*
homeassistant.components.sensor.* homeassistant.components.sensor.*
homeassistant.components.senseme.* homeassistant.components.senseme.*
homeassistant.components.senz.* homeassistant.components.senz.*
@ -224,6 +227,7 @@ homeassistant.components.tplink.*
homeassistant.components.tolo.* homeassistant.components.tolo.*
homeassistant.components.tractive.* homeassistant.components.tractive.*
homeassistant.components.tradfri.* homeassistant.components.tradfri.*
homeassistant.components.trafikverket_ferry.*
homeassistant.components.trafikverket_train.* homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.* homeassistant.components.trafikverket_weatherstation.*
homeassistant.components.tts.* homeassistant.components.tts.*

View File

@ -6,6 +6,7 @@
# Home Assistant Core # Home Assistant Core
setup.cfg @home-assistant/core setup.cfg @home-assistant/core
pyproject.toml @home-assistant/core
/homeassistant/*.py @home-assistant/core /homeassistant/*.py @home-assistant/core
/homeassistant/helpers/ @home-assistant/core /homeassistant/helpers/ @home-assistant/core
/homeassistant/util/ @home-assistant/core /homeassistant/util/ @home-assistant/core
@ -128,8 +129,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/binary_sensor/ @home-assistant/core /homeassistant/components/binary_sensor/ @home-assistant/core
/tests/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria /homeassistant/components/bizkaibus/ @UgaitzEtxebarria
/homeassistant/components/blebox/ @bbx-a @bbx-jp /homeassistant/components/blebox/ @bbx-a @bbx-jp @riokuu
/tests/components/blebox/ @bbx-a @bbx-jp /tests/components/blebox/ @bbx-a @bbx-jp @riokuu
/homeassistant/components/blink/ @fronzbot /homeassistant/components/blink/ @fronzbot
/tests/components/blink/ @fronzbot /tests/components/blink/ @fronzbot
/homeassistant/components/blueprint/ @home-assistant/core /homeassistant/components/blueprint/ @home-assistant/core
@ -272,6 +273,7 @@ build.json @home-assistant/supervisor
/tests/components/efergy/ @tkdrob /tests/components/efergy/ @tkdrob
/homeassistant/components/egardia/ @jeroenterheerdt /homeassistant/components/egardia/ @jeroenterheerdt
/homeassistant/components/eight_sleep/ @mezz64 @raman325 /homeassistant/components/eight_sleep/ @mezz64 @raman325
/tests/components/eight_sleep/ @mezz64 @raman325
/homeassistant/components/elgato/ @frenck /homeassistant/components/elgato/ @frenck
/tests/components/elgato/ @frenck /tests/components/elgato/ @frenck
/homeassistant/components/elkm1/ @gwww @bdraco /homeassistant/components/elkm1/ @gwww @bdraco
@ -283,6 +285,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/emoncms/ @borpin /homeassistant/components/emoncms/ @borpin
/homeassistant/components/emonitor/ @bdraco /homeassistant/components/emonitor/ @bdraco
/tests/components/emonitor/ @bdraco /tests/components/emonitor/ @bdraco
/homeassistant/components/emulated_hue/ @bdraco
/tests/components/emulated_hue/ @bdraco
/homeassistant/components/emulated_kasa/ @kbickar /homeassistant/components/emulated_kasa/ @kbickar
/tests/components/emulated_kasa/ @kbickar /tests/components/emulated_kasa/ @kbickar
/homeassistant/components/energy/ @home-assistant/core /homeassistant/components/energy/ @home-assistant/core
@ -327,7 +331,6 @@ build.json @home-assistant/supervisor
/tests/components/firmata/ @DaAwesomeP /tests/components/firmata/ @DaAwesomeP
/homeassistant/components/fivem/ @Sander0542 /homeassistant/components/fivem/ @Sander0542
/tests/components/fivem/ @Sander0542 /tests/components/fivem/ @Sander0542
/homeassistant/components/fixer/ @fabaff
/homeassistant/components/fjaraskupan/ @elupus /homeassistant/components/fjaraskupan/ @elupus
/tests/components/fjaraskupan/ @elupus /tests/components/fjaraskupan/ @elupus
/homeassistant/components/flick_electric/ @ZephireNZ /homeassistant/components/flick_electric/ @ZephireNZ
@ -363,6 +366,7 @@ build.json @home-assistant/supervisor
/tests/components/fronius/ @nielstron @farmio /tests/components/fronius/ @nielstron @farmio
/homeassistant/components/frontend/ @home-assistant/frontend /homeassistant/components/frontend/ @home-assistant/frontend
/tests/components/frontend/ @home-assistant/frontend /tests/components/frontend/ @home-assistant/frontend
/homeassistant/components/frontier_silicon/ @wlcrs
/homeassistant/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gdacs/ @exxamalte /homeassistant/components/gdacs/ @exxamalte
@ -445,6 +449,8 @@ build.json @home-assistant/supervisor
/tests/components/home_plus_control/ @chemaaa /tests/components/home_plus_control/ @chemaaa
/homeassistant/components/homeassistant/ @home-assistant/core /homeassistant/components/homeassistant/ @home-assistant/core
/tests/components/homeassistant/ @home-assistant/core /tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_yellow/ @home-assistant/core
/tests/components/homeassistant_yellow/ @home-assistant/core
/homeassistant/components/homekit/ @bdraco /homeassistant/components/homekit/ @bdraco
/tests/components/homekit/ @bdraco /tests/components/homekit/ @bdraco
/homeassistant/components/homekit_controller/ @Jc2k @bdraco /homeassistant/components/homekit_controller/ @Jc2k @bdraco
@ -465,8 +471,8 @@ build.json @home-assistant/supervisor
/tests/components/huisbaasje/ @dennisschroer /tests/components/huisbaasje/ @dennisschroer
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka /homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
/tests/components/humidifier/ @home-assistant/core @Shulyaka /tests/components/humidifier/ @home-assistant/core @Shulyaka
/homeassistant/components/hunterdouglas_powerview/ @bdraco @trullock /homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/tests/components/hunterdouglas_powerview/ @bdraco @trullock /tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/hvv_departures/ @vigonotion /homeassistant/components/hvv_departures/ @vigonotion
/tests/components/hvv_departures/ @vigonotion /tests/components/hvv_departures/ @vigonotion
/homeassistant/components/hydrawise/ @ptcryan /homeassistant/components/hydrawise/ @ptcryan
@ -474,8 +480,6 @@ build.json @home-assistant/supervisor
/tests/components/hyperion/ @dermotduffy /tests/components/hyperion/ @dermotduffy
/homeassistant/components/ialarm/ @RyuzakiKK /homeassistant/components/ialarm/ @RyuzakiKK
/tests/components/ialarm/ @RyuzakiKK /tests/components/ialarm/ @RyuzakiKK
/homeassistant/components/ialarm_xr/ @bigmoby
/tests/components/ialarm_xr/ @bigmoby
/homeassistant/components/iammeter/ @lewei50 /homeassistant/components/iammeter/ @lewei50
/homeassistant/components/iaqualink/ @flz /homeassistant/components/iaqualink/ @flz
/tests/components/iaqualink/ @flz /tests/components/iaqualink/ @flz
@ -568,6 +572,7 @@ build.json @home-assistant/supervisor
/tests/components/lcn/ @alengwenus /tests/components/lcn/ @alengwenus
/homeassistant/components/lg_netcast/ @Drafteed /homeassistant/components/lg_netcast/ @Drafteed
/homeassistant/components/life360/ @pnbruckner /homeassistant/components/life360/ @pnbruckner
/tests/components/life360/ @pnbruckner
/homeassistant/components/lifx/ @Djelibeybi /homeassistant/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core /homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core
@ -824,7 +829,8 @@ build.json @home-assistant/supervisor
/tests/components/rachio/ @bdraco /tests/components/rachio/ @bdraco
/homeassistant/components/radio_browser/ @frenck /homeassistant/components/radio_browser/ @frenck
/tests/components/radio_browser/ @frenck /tests/components/radio_browser/ @frenck
/homeassistant/components/radiotherm/ @vinnyfuria /homeassistant/components/radiotherm/ @bdraco @vinnyfuria
/tests/components/radiotherm/ @bdraco @vinnyfuria
/homeassistant/components/rainbird/ @konikvranik /homeassistant/components/rainbird/ @konikvranik
/homeassistant/components/raincloud/ @vanstinator /homeassistant/components/raincloud/ @vanstinator
/homeassistant/components/rainforest_eagle/ @gtdiehl @jcalbert @hastarin /homeassistant/components/rainforest_eagle/ @gtdiehl @jcalbert @hastarin
@ -924,6 +930,8 @@ build.json @home-assistant/supervisor
/tests/components/sighthound/ @robmarkcole /tests/components/sighthound/ @robmarkcole
/homeassistant/components/signal_messenger/ @bbernhard /homeassistant/components/signal_messenger/ @bbernhard
/tests/components/signal_messenger/ @bbernhard /tests/components/signal_messenger/ @bbernhard
/homeassistant/components/simplepush/ @engrbm87
/tests/components/simplepush/ @engrbm87
/homeassistant/components/simplisafe/ @bachya /homeassistant/components/simplisafe/ @bachya
/tests/components/simplisafe/ @bachya /tests/components/simplisafe/ @bachya
/homeassistant/components/sinch/ @bendikrb /homeassistant/components/sinch/ @bendikrb
@ -931,6 +939,8 @@ build.json @home-assistant/supervisor
/tests/components/siren/ @home-assistant/core @raman325 /tests/components/siren/ @home-assistant/core @raman325
/homeassistant/components/sisyphus/ @jkeljo /homeassistant/components/sisyphus/ @jkeljo
/homeassistant/components/sky_hub/ @rogerselwyn /homeassistant/components/sky_hub/ @rogerselwyn
/homeassistant/components/skybell/ @tkdrob
/tests/components/skybell/ @tkdrob
/homeassistant/components/slack/ @bachya @tkdrob /homeassistant/components/slack/ @bachya @tkdrob
/tests/components/slack/ @bachya @tkdrob /tests/components/slack/ @bachya @tkdrob
/homeassistant/components/sleepiq/ @mfugate1 @kbickar /homeassistant/components/sleepiq/ @mfugate1 @kbickar
@ -961,8 +971,6 @@ build.json @home-assistant/supervisor
/tests/components/solax/ @squishykid /tests/components/solax/ @squishykid
/homeassistant/components/soma/ @ratsept @sebfortier2288 /homeassistant/components/soma/ @ratsept @sebfortier2288
/tests/components/soma/ @ratsept @sebfortier2288 /tests/components/soma/ @ratsept @sebfortier2288
/homeassistant/components/somfy/ @tetienne
/tests/components/somfy/ @tetienne
/homeassistant/components/sonarr/ @ctalkington /homeassistant/components/sonarr/ @ctalkington
/tests/components/sonarr/ @ctalkington /tests/components/sonarr/ @ctalkington
/homeassistant/components/songpal/ @rytilahti @shenxn /homeassistant/components/songpal/ @rytilahti @shenxn
@ -1061,12 +1069,12 @@ build.json @home-assistant/supervisor
/tests/components/todoist/ @boralyl /tests/components/todoist/ @boralyl
/homeassistant/components/tolo/ @MatthiasLohr /homeassistant/components/tolo/ @MatthiasLohr
/tests/components/tolo/ @MatthiasLohr /tests/components/tolo/ @MatthiasLohr
/homeassistant/components/tomorrowio/ @raman325 /homeassistant/components/tomorrowio/ @raman325 @lymanepp
/tests/components/tomorrowio/ @raman325 /tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek /homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek /tests/components/totalconnect/ @austinmroczek
/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco /homeassistant/components/tplink/ @rytilahti @thegardenmonkey
/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco /tests/components/tplink/ @rytilahti @thegardenmonkey
/homeassistant/components/traccar/ @ludeeus /homeassistant/components/traccar/ @ludeeus
/tests/components/traccar/ @ludeeus /tests/components/traccar/ @ludeeus
/homeassistant/components/trace/ @home-assistant/core /homeassistant/components/trace/ @home-assistant/core
@ -1157,8 +1165,8 @@ build.json @home-assistant/supervisor
/tests/components/watttime/ @bachya /tests/components/watttime/ @bachya
/homeassistant/components/waze_travel_time/ @eifinger /homeassistant/components/waze_travel_time/ @eifinger
/tests/components/waze_travel_time/ @eifinger /tests/components/waze_travel_time/ @eifinger
/homeassistant/components/weather/ @fabaff /homeassistant/components/weather/ @home-assistant/core
/tests/components/weather/ @fabaff /tests/components/weather/ @home-assistant/core
/homeassistant/components/webhook/ @home-assistant/core /homeassistant/components/webhook/ @home-assistant/core
/tests/components/webhook/ @home-assistant/core /tests/components/webhook/ @home-assistant/core
/homeassistant/components/webostv/ @bendavid @thecode /homeassistant/components/webostv/ @bendavid @thecode

View File

@ -123,7 +123,7 @@ enforcement ladder][mozilla].
## Adoption ## Adoption
This Code of Conduct was first adopted January 21st, 2017 and announced in This Code of Conduct was first adopted on January 21st, 2017, and announced in
[this][coc-blog] blog post and has been updated on May 25th, 2020 to version [this][coc-blog] blog post and has been updated on May 25th, 2020 to version
2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog] 2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog]
blog post. blog post.

View File

@ -25,21 +25,6 @@ RUN \
-e ./homeassistant --use-deprecated=legacy-resolver \ -e ./homeassistant --use-deprecated=legacy-resolver \
&& python3 -m compileall homeassistant/homeassistant && python3 -m compileall homeassistant/homeassistant
# Fix Bug with Alpine 3.14 and sqlite 3.35
# https://gitlab.alpinelinux.org/alpine/aports/-/issues/12524
ARG BUILD_ARCH
RUN \
if [ "${BUILD_ARCH}" = "amd64" ]; then \
export APK_ARCH=x86_64; \
elif [ "${BUILD_ARCH}" = "i386" ]; then \
export APK_ARCH=x86; \
else \
export APK_ARCH=${BUILD_ARCH}; \
fi \
&& curl -O http://dl-cdn.alpinelinux.org/alpine/v3.13/main/${APK_ARCH}/sqlite-libs-3.34.1-r0.apk \
&& apk add --no-cache sqlite-libs-3.34.1-r0.apk \
&& rm -f sqlite-libs-3.34.1-r0.apk
# Home Assistant S6-Overlay # Home Assistant S6-Overlay
COPY rootfs / COPY rootfs /

View File

@ -18,6 +18,7 @@ RUN \
libavfilter-dev \ libavfilter-dev \
libpcap-dev \ libpcap-dev \
libturbojpeg0 \ libturbojpeg0 \
libyaml-dev \
libxml2 \ libxml2 \
git \ git \
cmake \ cmake \

View File

@ -1,11 +1,11 @@
image: homeassistant/{arch}-homeassistant image: homeassistant/{arch}-homeassistant
shadow_repository: ghcr.io/home-assistant shadow_repository: ghcr.io/home-assistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.05.0 aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.06.2
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.05.0 armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.06.2
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.05.0 armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.06.2
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.05.0 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.06.2
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.05.0 i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.06.2
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

@ -20,6 +20,7 @@ from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
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"
EVENT_USER_UPDATED = "user_updated"
EVENT_USER_REMOVED = "user_removed" EVENT_USER_REMOVED = "user_removed"
_MfaModuleDict = dict[str, MultiFactorAuthModule] _MfaModuleDict = dict[str, MultiFactorAuthModule]
@ -103,7 +104,7 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
"""Return a user as result of login flow.""" """Return a user as result of login flow."""
flow = cast(LoginFlow, flow) flow = cast(LoginFlow, flow)
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
return result return result
# we got final result # we got final result
@ -338,6 +339,8 @@ class AuthManager:
else: else:
await self.async_deactivate_user(user) await self.async_deactivate_user(user)
self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id})
async def async_activate_user(self, user: models.User) -> None: async def async_activate_user(self, user: models.User) -> None:
"""Activate a user.""" """Activate a user."""
await self._store.async_activate_user(user) await self._store.async_activate_user(user)

View File

@ -272,7 +272,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
if not errors: if not errors:
return await self.async_finish(self.credential) return await self.async_finish(self.credential)
description_placeholders: dict[str, str | None] = { description_placeholders: dict[str, str] = {
"mfa_module_name": auth_module.name, "mfa_module_name": auth_module.name,
"mfa_module_id": auth_module.id, "mfa_module_id": auth_module.id,
} }

View File

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
import logging import logging
import logging.handlers import logging.handlers
import os import os
import platform
import sys import sys
import threading import threading
from time import monotonic from time import monotonic
@ -398,7 +399,7 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
domains.update(hass.config_entries.async_domains()) domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded # Make sure the Hass.io component is loaded
if "HASSIO" in os.environ: if "SUPERVISOR" in os.environ:
domains.add("hassio") domains.add("hassio")
return domains return domains
@ -540,11 +541,22 @@ async def _async_set_up_integrations(
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
def _cache_uname_processor() -> None:
"""Cache the result of platform.uname().processor in the executor.
Multiple modules call this function at startup which
executes a blocking subprocess call. This is a problem for the
asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop.
"""
platform.uname().processor # pylint: disable=expression-not-assigned
# Load the registries # Load the registries
await asyncio.gather( await asyncio.gather(
device_registry.async_load(hass), device_registry.async_load(hass),
entity_registry.async_load(hass), entity_registry.async_load(hass),
area_registry.async_load(hass), area_registry.async_load(hass),
hass.async_add_executor_job(_cache_uname_processor),
) )
# Start setup # Start setup

View File

@ -1,6 +1,7 @@
"""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 http import HTTPStatus from http import HTTPStatus
from typing import Any, cast from typing import Any, cast
@ -149,9 +150,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, config: dict[str, Any]) -> FlowResult: async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle reauthorization request from Abode.""" """Handle reauthorization request from Abode."""
self._username = config[CONF_USERNAME] self._username = entry_data[CONF_USERNAME]
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()

View File

@ -11,9 +11,7 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
ATTR_HS_COLOR, ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS, ColorMode,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
LightEntity, LightEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -101,11 +99,27 @@ class AbodeLight(AbodeDevice, LightEntity):
_hs = self._device.color _hs = self._device.color
return _hs return _hs
@property
def color_mode(self) -> str | None:
"""Return the color mode of the light."""
if self._device.is_dimmable and self._device.is_color_capable:
if self.hs_color is not None:
return ColorMode.HS
return ColorMode.COLOR_TEMP
if self._device.is_dimmable:
return ColorMode.BRIGHTNESS
return ColorMode.ONOFF
@property
def supported_color_modes(self) -> set[str] | None:
"""Flag supported color modes."""
if self._device.is_dimmable and self._device.is_color_capable:
return {ColorMode.COLOR_TEMP, ColorMode.HS}
if self._device.is_dimmable:
return {ColorMode.BRIGHTNESS}
return {ColorMode.ONOFF}
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
if self._device.is_dimmable and self._device.is_color_capable:
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP
if self._device.is_dimmable:
return SUPPORT_BRIGHTNESS
return 0 return 0

View File

@ -4,6 +4,11 @@
"single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten." "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten."
}, },
"step": { "step": {
"reauth_confirm": {
"data": {
"username": "E-postadress"
}
},
"user": { "user": {
"data": { "data": {
"password": "L\u00f6senord", "password": "L\u00f6senord",

View File

@ -3,6 +3,9 @@
"abort": { "abort": {
"single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
}, },
"create_entry": {
"default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\n El pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n."
},
"error": { "error": {
"cannot_connect": "No se pudo conectar", "cannot_connect": "No se pudo conectar",
"invalid_api_key": "Clave API no v\u00e1lida", "invalid_api_key": "Clave API no v\u00e1lida",

View File

@ -3,6 +3,9 @@
"abort": { "abort": {
"single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea."
}, },
"create_entry": {
"default": "\u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d0\u05d9\u05e0\u05dd \u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05d1\u05d9\u05db\u05d5\u05dc\u05ea\u05da \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d5\u05ea\u05dd \u05d1\u05e8\u05d9\u05e9\u05d5\u05dd \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05dc\u05d0\u05d7\u05e8 \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1.\n \u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d4\u05d0\u05d5\u05d5\u05d9\u05e8 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc\u05ea \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05d1\u05d9\u05db\u05d5\u05dc\u05ea\u05da \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05ea \u05d6\u05d4 \u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1."
},
"error": { "error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "API-nyckel"
}
}
}
}
}

View File

@ -6,19 +6,26 @@ from typing import Any, cast
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
Forecast, Forecast,
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_MILES,
LENGTH_MILLIMETERS,
PRESSURE_HPA,
PRESSURE_INHG,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR, SPEED_MILES_PER_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
@ -66,19 +73,25 @@ class AccuWeatherEntity(
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL # Coordinator data is used also for sensors which don't have units automatically
wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][ # converted, hence the weather entity's native units follow the configured unit
"Unit" # system
] if coordinator.is_metric:
if wind_speed_unit == "mi/h": self._attr_native_precipitation_unit = LENGTH_MILLIMETERS
self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR self._attr_native_pressure_unit = PRESSURE_HPA
self._attr_native_temperature_unit = TEMP_CELSIUS
self._attr_native_visibility_unit = LENGTH_KILOMETERS
self._attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
self._unit_system = API_METRIC
else: else:
self._attr_wind_speed_unit = wind_speed_unit self._unit_system = API_IMPERIAL
self._attr_native_precipitation_unit = LENGTH_INCHES
self._attr_native_pressure_unit = PRESSURE_INHG
self._attr_native_temperature_unit = TEMP_FAHRENHEIT
self._attr_native_visibility_unit = LENGTH_MILES
self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR
self._attr_name = name self._attr_name = name
self._attr_unique_id = coordinator.location_key self._attr_unique_id = coordinator.location_key
self._attr_temperature_unit = (
TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT
)
self._attr_attribution = ATTRIBUTION self._attr_attribution = ATTRIBUTION
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
@ -106,14 +119,14 @@ class AccuWeatherEntity(
return None return None
@property @property
def temperature(self) -> float: def native_temperature(self) -> float:
"""Return the temperature.""" """Return the temperature."""
return cast( return cast(
float, self.coordinator.data["Temperature"][self._unit_system]["Value"] float, self.coordinator.data["Temperature"][self._unit_system]["Value"]
) )
@property @property
def pressure(self) -> float: def native_pressure(self) -> float:
"""Return the pressure.""" """Return the pressure."""
return cast( return cast(
float, self.coordinator.data["Pressure"][self._unit_system]["Value"] float, self.coordinator.data["Pressure"][self._unit_system]["Value"]
@ -125,7 +138,7 @@ class AccuWeatherEntity(
return cast(int, self.coordinator.data["RelativeHumidity"]) return cast(int, self.coordinator.data["RelativeHumidity"])
@property @property
def wind_speed(self) -> float: def native_wind_speed(self) -> float:
"""Return the wind speed.""" """Return the wind speed."""
return cast( return cast(
float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
@ -137,7 +150,7 @@ class AccuWeatherEntity(
return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"]) return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"])
@property @property
def visibility(self) -> float: def native_visibility(self) -> float:
"""Return the visibility.""" """Return the visibility."""
return cast( return cast(
float, self.coordinator.data["Visibility"][self._unit_system]["Value"] float, self.coordinator.data["Visibility"][self._unit_system]["Value"]
@ -162,9 +175,9 @@ class AccuWeatherEntity(
return [ return [
{ {
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"], ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"]["Value"],
ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"], ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"]["Value"],
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item), ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(item),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: round( ATTR_FORECAST_PRECIPITATION_PROBABILITY: round(
mean( mean(
[ [
@ -173,7 +186,7 @@ class AccuWeatherEntity(
] ]
) )
), ),
ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"], ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"]["Speed"]["Value"],
ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"], ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"],
ATTR_FORECAST_CONDITION: [ ATTR_FORECAST_CONDITION: [
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v

View File

@ -1,6 +1,8 @@
"""Support for Acmeda Roller Blinds.""" """Support for Acmeda Roller Blinds."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
CoverEntity, CoverEntity,
@ -45,7 +47,7 @@ class AcmedaCover(AcmedaBase, CoverEntity):
"""Representation of a Acmeda cover device.""" """Representation of a Acmeda cover device."""
@property @property
def current_cover_position(self): def current_cover_position(self) -> int | None:
"""Return the current position of the roller blind. """Return the current position of the roller blind.
None is unknown, 0 is closed, 100 is fully open. None is unknown, 0 is closed, 100 is fully open.
@ -56,7 +58,7 @@ class AcmedaCover(AcmedaBase, CoverEntity):
return position return position
@property @property
def current_cover_tilt_position(self): def current_cover_tilt_position(self) -> int | None:
"""Return the current tilt of the roller blind. """Return the current tilt of the roller blind.
None is unknown, 0 is closed, 100 is fully open. None is unknown, 0 is closed, 100 is fully open.
@ -67,7 +69,7 @@ class AcmedaCover(AcmedaBase, CoverEntity):
return position return position
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
supported_features = 0 supported_features = 0
if self.current_cover_position is not None: if self.current_cover_position is not None:
@ -88,35 +90,35 @@ class AcmedaCover(AcmedaBase, CoverEntity):
return supported_features return supported_features
@property @property
def is_closed(self): def is_closed(self) -> bool:
"""Return if the cover is closed.""" """Return if the cover is closed."""
return self.roller.closed_percent == 100 return self.roller.closed_percent == 100
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the roller.""" """Close the roller."""
await self.roller.move_down() await self.roller.move_down()
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the roller.""" """Open the roller."""
await self.roller.move_up() await self.roller.move_up()
async def async_stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the roller.""" """Stop the roller."""
await self.roller.move_stop() await self.roller.move_stop()
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the roller shutter to a specific position.""" """Move the roller shutter to a specific position."""
await self.roller.move_to(100 - kwargs[ATTR_POSITION]) await self.roller.move_to(100 - kwargs[ATTR_POSITION])
async def async_close_cover_tilt(self, **kwargs): async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the roller.""" """Close the roller."""
await self.roller.move_down() await self.roller.move_down()
async def async_open_cover_tilt(self, **kwargs): async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the roller.""" """Open the roller."""
await self.roller.move_up() await self.roller.move_up()
async def async_stop_cover_tilt(self, **kwargs): async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the roller.""" """Stop the roller."""
await self.roller.move_stop() await self.roller.move_stop()

View File

@ -115,14 +115,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload AdGuard Home config entry.""" """Unload AdGuard Home config entry."""
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
del hass.data[DOMAIN] del hass.data[DOMAIN]
return unload_ok return unload_ok

View File

@ -1,6 +1,8 @@
"""Support for ADS covers.""" """Support for ADS covers."""
from __future__ import annotations from __future__ import annotations
from typing import Any
import pyads import pyads
import voluptuous as vol import voluptuous as vol
@ -122,7 +124,7 @@ class AdsCover(AdsEntity, CoverEntity):
if ads_var_pos_set is not None: if ads_var_pos_set is not None:
self._attr_supported_features |= CoverEntityFeature.SET_POSITION self._attr_supported_features |= CoverEntityFeature.SET_POSITION
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Register device notification.""" """Register device notification."""
if self._ads_var is not None: if self._ads_var is not None:
await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL)
@ -133,7 +135,7 @@ class AdsCover(AdsEntity, CoverEntity):
) )
@property @property
def is_closed(self): def is_closed(self) -> bool | None:
"""Return if the cover is closed.""" """Return if the cover is closed."""
if self._ads_var is not None: if self._ads_var is not None:
return self._state_dict[STATE_KEY_STATE] return self._state_dict[STATE_KEY_STATE]
@ -142,16 +144,16 @@ class AdsCover(AdsEntity, CoverEntity):
return None return None
@property @property
def current_cover_position(self): def current_cover_position(self) -> int:
"""Return current position of cover.""" """Return current position of cover."""
return self._state_dict[STATE_KEY_POSITION] return self._state_dict[STATE_KEY_POSITION]
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs: Any) -> None:
"""Fire the stop action.""" """Fire the stop action."""
if self._ads_var_stop: if self._ads_var_stop:
self._ads_hub.write_by_name(self._ads_var_stop, True, pyads.PLCTYPE_BOOL) self._ads_hub.write_by_name(self._ads_var_stop, True, pyads.PLCTYPE_BOOL)
def set_cover_position(self, **kwargs): def set_cover_position(self, **kwargs: Any) -> None:
"""Set cover position.""" """Set cover position."""
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
if self._ads_var_pos_set is not None: if self._ads_var_pos_set is not None:
@ -159,14 +161,14 @@ class AdsCover(AdsEntity, CoverEntity):
self._ads_var_pos_set, position, pyads.PLCTYPE_BYTE self._ads_var_pos_set, position, pyads.PLCTYPE_BYTE
) )
def open_cover(self, **kwargs): def open_cover(self, **kwargs: Any) -> None:
"""Move the cover up.""" """Move the cover up."""
if self._ads_var_open is not None: if self._ads_var_open is not None:
self._ads_hub.write_by_name(self._ads_var_open, True, pyads.PLCTYPE_BOOL) self._ads_hub.write_by_name(self._ads_var_open, True, pyads.PLCTYPE_BOOL)
elif self._ads_var_pos_set is not None: elif self._ads_var_pos_set is not None:
self.set_cover_position(position=100) self.set_cover_position(position=100)
def close_cover(self, **kwargs): def close_cover(self, **kwargs: Any) -> None:
"""Move the cover down.""" """Move the cover down."""
if self._ads_var_close is not None: if self._ads_var_close is not None:
self._ads_hub.write_by_name(self._ads_var_close, True, pyads.PLCTYPE_BOOL) self._ads_hub.write_by_name(self._ads_var_close, True, pyads.PLCTYPE_BOOL)

View File

@ -1,4 +1,6 @@
"""Cover platform for Advantage Air integration.""" """Cover platform for Advantage Air integration."""
from typing import Any
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
CoverDeviceClass, CoverDeviceClass,
@ -56,18 +58,18 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity):
) )
@property @property
def is_closed(self): def is_closed(self) -> bool:
"""Return if vent is fully closed.""" """Return if vent is fully closed."""
return self._zone["state"] == ADVANTAGE_AIR_STATE_CLOSE return self._zone["state"] == ADVANTAGE_AIR_STATE_CLOSE
@property @property
def current_cover_position(self): def current_cover_position(self) -> int:
"""Return vents current position as a percentage.""" """Return vents current position as a percentage."""
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
return self._zone["value"] return self._zone["value"]
return 0 return 0
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs: Any) -> None:
"""Fully open zone vent.""" """Fully open zone vent."""
await self.async_change( await self.async_change(
{ {
@ -79,7 +81,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity):
} }
) )
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs: Any) -> None:
"""Fully close zone vent.""" """Fully close zone vent."""
await self.async_change( await self.async_change(
{ {
@ -89,7 +91,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity):
} }
) )
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Change vent position.""" """Change vent position."""
position = round(kwargs[ATTR_POSITION] / 5) * 5 position = round(kwargs[ATTR_POSITION] / 5) * 5
if position == 0: if position == 0:

View File

@ -1,4 +1,6 @@
"""Config flow for AEMET OpenData.""" """Config flow for AEMET OpenData."""
from __future__ import annotations
from aemet_opendata import AEMET from aemet_opendata import AEMET
import voluptuous as vol import voluptuous as vol
@ -50,7 +52,9 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler(config_entry) return OptionsFlowHandler(config_entry)

View File

@ -17,14 +17,6 @@ from homeassistant.components.weather import (
ATTR_CONDITION_RAINY, ATTR_CONDITION_RAINY,
ATTR_CONDITION_SNOWY, ATTR_CONDITION_SNOWY,
ATTR_CONDITION_SUNNY, ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
) )
from homeassistant.const import ( from homeassistant.const import (
DEGREE, DEGREE,
@ -45,8 +37,16 @@ ENTRY_NAME = "name"
ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
ATTR_API_CONDITION = "condition" ATTR_API_CONDITION = "condition"
ATTR_API_FORECAST_CONDITION = "condition"
ATTR_API_FORECAST_DAILY = "forecast-daily" ATTR_API_FORECAST_DAILY = "forecast-daily"
ATTR_API_FORECAST_HOURLY = "forecast-hourly" ATTR_API_FORECAST_HOURLY = "forecast-hourly"
ATTR_API_FORECAST_PRECIPITATION = "precipitation"
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability"
ATTR_API_FORECAST_TEMP = "temperature"
ATTR_API_FORECAST_TEMP_LOW = "templow"
ATTR_API_FORECAST_TIME = "datetime"
ATTR_API_FORECAST_WIND_BEARING = "wind_bearing"
ATTR_API_FORECAST_WIND_SPEED = "wind_speed"
ATTR_API_HUMIDITY = "humidity" ATTR_API_HUMIDITY = "humidity"
ATTR_API_PRESSURE = "pressure" ATTR_API_PRESSURE = "pressure"
ATTR_API_RAIN = "rain" ATTR_API_RAIN = "rain"
@ -158,14 +158,14 @@ CONDITIONS_MAP = {
} }
FORECAST_MONITORED_CONDITIONS = [ FORECAST_MONITORED_CONDITIONS = [
ATTR_FORECAST_CONDITION, ATTR_API_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION, ATTR_API_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP, ATTR_API_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW, ATTR_API_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME, ATTR_API_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_API_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED, ATTR_API_FORECAST_WIND_SPEED,
] ]
MONITORED_CONDITIONS = [ MONITORED_CONDITIONS = [
ATTR_API_CONDITION, ATTR_API_CONDITION,
@ -202,43 +202,43 @@ FORECAST_MODE_ATTR_API = {
FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_CONDITION, key=ATTR_API_FORECAST_CONDITION,
name="Condition", name="Condition",
), ),
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_PRECIPITATION, key=ATTR_API_FORECAST_PRECIPITATION,
name="Precipitation", name="Precipitation",
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
), ),
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
name="Precipitation probability", name="Precipitation probability",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_TEMP, key=ATTR_API_FORECAST_TEMP,
name="Temperature", name="Temperature",
native_unit_of_measurement=TEMP_CELSIUS, native_unit_of_measurement=TEMP_CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_TEMP_LOW, key=ATTR_API_FORECAST_TEMP_LOW,
name="Temperature Low", name="Temperature Low",
native_unit_of_measurement=TEMP_CELSIUS, native_unit_of_measurement=TEMP_CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_TIME, key=ATTR_API_FORECAST_TIME,
name="Time", name="Time",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
), ),
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_WIND_BEARING, key=ATTR_API_FORECAST_WIND_BEARING,
name="Wind bearing", name="Wind bearing",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=ATTR_FORECAST_WIND_SPEED, key=ATTR_API_FORECAST_WIND_SPEED,
name="Wind speed", name="Wind speed",
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
), ),

View File

@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( from .const import (
ATTR_FORECAST_TIME, ATTR_API_FORECAST_TIME,
ATTRIBUTION, ATTRIBUTION,
DOMAIN, DOMAIN,
ENTRY_NAME, ENTRY_NAME,
@ -45,17 +45,13 @@ async def async_setup_entry(
entities.extend( entities.extend(
[ [
AemetForecastSensor( AemetForecastSensor(
name_prefix, f"{domain_data[ENTRY_NAME]} {mode} Forecast",
unique_id_prefix, f"{unique_id}-forecast-{mode}",
weather_coordinator, weather_coordinator,
mode, mode,
description, description,
) )
for mode in FORECAST_MODES for mode in FORECAST_MODES
if (
(name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast")
and (unique_id_prefix := f"{unique_id}-forecast-{mode}")
)
for description in FORECAST_SENSOR_TYPES for description in FORECAST_SENSOR_TYPES
if description.key in FORECAST_MONITORED_CONDITIONS if description.key in FORECAST_MONITORED_CONDITIONS
] ]
@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor):
def __init__( def __init__(
self, self,
name, name,
unique_id, unique_id_prefix,
weather_coordinator: WeatherUpdateCoordinator, weather_coordinator: WeatherUpdateCoordinator,
description: SensorEntityDescription, description: SensorEntityDescription,
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__( super().__init__(
name=name, name=name,
unique_id=f"{unique_id}-{description.key}", unique_id=f"{unique_id_prefix}-{description.key}",
coordinator=weather_coordinator, coordinator=weather_coordinator,
description=description, description=description,
) )
@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor):
def __init__( def __init__(
self, self,
name, name,
unique_id, unique_id_prefix,
weather_coordinator: WeatherUpdateCoordinator, weather_coordinator: WeatherUpdateCoordinator,
forecast_mode, forecast_mode,
description: SensorEntityDescription, description: SensorEntityDescription,
@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__( super().__init__(
name=name, name=name,
unique_id=f"{unique_id}-{description.key}", unique_id=f"{unique_id_prefix}-{description.key}",
coordinator=weather_coordinator, coordinator=weather_coordinator,
description=description, description=description,
) )
@ -139,6 +135,6 @@ class AemetForecastSensor(AbstractAemetSensor):
) )
if forecasts: if forecasts:
forecast = forecasts[0].get(self.entity_description.key) forecast = forecasts[0].get(self.entity_description.key)
if self.entity_description.key == ATTR_FORECAST_TIME: if self.entity_description.key == ATTR_API_FORECAST_TIME:
forecast = dt_util.parse_datetime(forecast) forecast = dt_util.parse_datetime(forecast)
return forecast return forecast

View File

@ -1,5 +1,8 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e"
},
"error": { "error": {
"invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447"
}, },

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "API-nyckel"
}
}
}
}
}

View File

@ -1,13 +1,36 @@
"""Support for the AEMET OpenData service.""" """Support for the AEMET OpenData service."""
from homeassistant.components.weather import WeatherEntity from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.const import (
LENGTH_MILLIMETERS,
PRESSURE_HPA,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import (
ATTR_API_CONDITION, ATTR_API_CONDITION,
ATTR_API_FORECAST_CONDITION,
ATTR_API_FORECAST_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_TEMP,
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY, ATTR_API_HUMIDITY,
ATTR_API_PRESSURE, ATTR_API_PRESSURE,
ATTR_API_TEMPERATURE, ATTR_API_TEMPERATURE,
@ -19,10 +42,32 @@ from .const import (
ENTRY_WEATHER_COORDINATOR, ENTRY_WEATHER_COORDINATOR,
FORECAST_MODE_ATTR_API, FORECAST_MODE_ATTR_API,
FORECAST_MODE_DAILY, FORECAST_MODE_DAILY,
FORECAST_MODE_HOURLY,
FORECAST_MODES, FORECAST_MODES,
) )
from .weather_update_coordinator import WeatherUpdateCoordinator from .weather_update_coordinator import WeatherUpdateCoordinator
FORECAST_MAP = {
FORECAST_MODE_DAILY: {
ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP,
ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED,
},
FORECAST_MODE_HOURLY: {
ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP,
ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED,
},
}
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -47,9 +92,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
"""Implementation of an AEMET OpenData sensor.""" """Implementation of an AEMET OpenData sensor."""
_attr_attribution = ATTRIBUTION _attr_attribution = ATTRIBUTION
_attr_temperature_unit = TEMP_CELSIUS _attr_native_precipitation_unit = LENGTH_MILLIMETERS
_attr_pressure_unit = PRESSURE_HPA _attr_native_pressure_unit = PRESSURE_HPA
_attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR _attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
def __init__( def __init__(
self, self,
@ -75,7 +121,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
@property @property
def forecast(self): def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
forecast_map = FORECAST_MAP[self._forecast_mode]
return [
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
for forecast in forecasts
]
@property @property
def humidity(self): def humidity(self):
@ -83,12 +134,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
return self.coordinator.data[ATTR_API_HUMIDITY] return self.coordinator.data[ATTR_API_HUMIDITY]
@property @property
def pressure(self): def native_pressure(self):
"""Return the pressure.""" """Return the pressure."""
return self.coordinator.data[ATTR_API_PRESSURE] return self.coordinator.data[ATTR_API_PRESSURE]
@property @property
def temperature(self): def native_temperature(self):
"""Return the temperature.""" """Return the temperature."""
return self.coordinator.data[ATTR_API_TEMPERATURE] return self.coordinator.data[ATTR_API_TEMPERATURE]
@ -98,6 +149,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
return self.coordinator.data[ATTR_API_WIND_BEARING] return self.coordinator.data[ATTR_API_WIND_BEARING]
@property @property
def wind_speed(self): def native_wind_speed(self):
"""Return the wind speed.""" """Return the wind speed."""
return self.coordinator.data[ATTR_API_WIND_SPEED] return self.coordinator.data[ATTR_API_WIND_SPEED]

View File

@ -42,23 +42,21 @@ from aemet_opendata.helpers import (
) )
import async_timeout import async_timeout
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( from .const import (
ATTR_API_CONDITION, ATTR_API_CONDITION,
ATTR_API_FORECAST_CONDITION,
ATTR_API_FORECAST_DAILY, ATTR_API_FORECAST_DAILY,
ATTR_API_FORECAST_HOURLY, ATTR_API_FORECAST_HOURLY,
ATTR_API_FORECAST_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_TEMP,
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY, ATTR_API_HUMIDITY,
ATTR_API_PRESSURE, ATTR_API_PRESSURE,
ATTR_API_RAIN, ATTR_API_RAIN,
@ -402,15 +400,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
return None return None
return { return {
ATTR_FORECAST_CONDITION: condition, ATTR_API_FORECAST_CONDITION: condition,
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day(
day day
), ),
ATTR_FORECAST_TEMP: self._get_temperature_day(day), ATTR_API_FORECAST_TEMP: self._get_temperature_day(day),
ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), ATTR_API_FORECAST_TEMP_LOW: self._get_temperature_low_day(day),
ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), ATTR_API_FORECAST_TIME: dt_util.as_utc(date).isoformat(),
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed_day(day),
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day),
} }
def _convert_forecast_hour(self, date, day, hour): def _convert_forecast_hour(self, date, day, hour):
@ -420,15 +418,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
forecast_dt = date.replace(hour=hour, minute=0, second=0) forecast_dt = date.replace(hour=hour, minute=0, second=0)
return { return {
ATTR_FORECAST_CONDITION: condition, ATTR_API_FORECAST_CONDITION: condition,
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob(
day, hour day, hour
), ),
ATTR_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour),
ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(),
ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
} }
def _calc_precipitation(self, day, hour): def _calc_precipitation(self, day, hour):

View File

@ -1,4 +1,6 @@
"""Support for Agent DVR Alarm Control Panels.""" """Support for Agent DVR Alarm Control Panels."""
from __future__ import annotations
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity, AlarmControlPanelEntity,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
@ -58,7 +60,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
sw_version=client.version, sw_version=client.version,
) )
async def async_update(self): async def async_update(self) -> None:
"""Update the state of the device.""" """Update the state of the device."""
await self._client.update() await self._client.update()
self._attr_available = self._client.is_available self._attr_available = self._client.is_available
@ -76,24 +78,24 @@ class AgentBaseStation(AlarmControlPanelEntity):
else: else:
self._attr_state = STATE_ALARM_DISARMED self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_disarm(self, code=None): async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
await self._client.disarm() await self._client.disarm()
self._attr_state = STATE_ALARM_DISARMED self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_arm_away(self, code=None): async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command. Uses custom mode.""" """Send arm away command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_AWAY_MODE_NAME) await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
self._attr_state = STATE_ALARM_ARMED_AWAY self._attr_state = STATE_ALARM_ARMED_AWAY
async def async_alarm_arm_home(self, code=None): async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command. Uses custom mode.""" """Send arm home command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_HOME_MODE_NAME) await self._client.set_active_profile(CONF_HOME_MODE_NAME)
self._attr_state = STATE_ALARM_ARMED_HOME self._attr_state = STATE_ALARM_ARMED_HOME
async def async_alarm_arm_night(self, code=None): async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command. Uses custom mode.""" """Send arm night command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME) await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)

View File

@ -3,6 +3,9 @@
"abort": { "abort": {
"already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e"
}, },
"error": {
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435"
},
"step": { "step": {
"user": { "user": {
"data": { "data": {

View File

@ -4,6 +4,7 @@
"already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." "already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad."
}, },
"error": { "error": {
"invalid_api_key": "Ogiltig API-nyckel",
"wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de." "wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de."
}, },
"step": { "step": {

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "API-nyckel"
}
}
}
}
}

View File

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Mapping
from typing import Any
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import ( from pyairvisual.errors import (
@ -70,7 +72,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the config flow.""" """Initialize the config flow."""
self._entry_data_for_reauth: dict[str, str] = {} self._entry_data_for_reauth: Mapping[str, Any] = {}
self._geo_id: str | None = None self._geo_id: str | None = None
@property @property
@ -219,10 +221,10 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO},
) )
async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle configuration by re-auth.""" """Handle configuration by re-auth."""
self._entry_data_for_reauth = data self._entry_data_for_reauth = entry_data
self._geo_id = async_get_geography_id(data) self._geo_id = async_get_geography_id(entry_data)
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm( async def async_step_reauth_confirm(

View File

@ -11,7 +11,8 @@
"step": { "step": {
"geography_by_coords": { "geography_by_coords": {
"data": { "data": {
"api_key": "API \u043a\u043b\u044e\u0447" "api_key": "API \u043a\u043b\u044e\u0447",
"longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430"
} }
}, },
"geography_by_name": { "geography_by_name": {

View File

@ -1,5 +1,8 @@
{ {
"state": { "state": {
"airvisual__pollutant_label": {
"p1": "PM10"
},
"airvisual__pollutant_level": { "airvisual__pollutant_level": {
"good": "\u05d8\u05d5\u05d1", "good": "\u05d8\u05d5\u05d1",
"unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0", "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0",

View File

@ -0,0 +1,7 @@
{
"state": {
"airvisual__pollutant_label": {
"co": "Kolmonoxid"
}
}
}

View File

@ -5,6 +5,11 @@
"invalid_api_key": "Ogiltig API-nyckel" "invalid_api_key": "Ogiltig API-nyckel"
}, },
"step": { "step": {
"geography_by_name": {
"data": {
"api_key": "API-nyckel"
}
},
"node_pro": { "node_pro": {
"data": { "data": {
"ip_address": "Enhets IP-adress / v\u00e4rdnamn", "ip_address": "Enhets IP-adress / v\u00e4rdnamn",

View File

@ -3,7 +3,7 @@
"name": "Airzone", "name": "Airzone",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"requirements": ["aioairzone==0.4.4"], "requirements": ["aioairzone==0.4.5"],
"codeowners": ["@Noltari"], "codeowners": ["@Noltari"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"] "loggers": ["aioairzone"]

View File

@ -1,13 +1,16 @@
"""The aladdin_connect component.""" """The aladdin_connect component."""
import asyncio
import logging import logging
from typing import Final from typing import Final
from aladdin_connect import AladdinConnectClient from AIOAladdinConnect import AladdinConnectClient
from aiohttp import ClientConnectionError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
@ -20,9 +23,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up platform from a ConfigEntry.""" """Set up platform from a ConfigEntry."""
username = entry.data[CONF_USERNAME] username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD] password = entry.data[CONF_PASSWORD]
acc = AladdinConnectClient(username, password) acc = AladdinConnectClient(username, password, async_get_clientsession(hass))
if not await hass.async_add_executor_job(acc.login): try:
raise ConfigEntryAuthFailed("Incorrect Password") if not await acc.login():
raise ConfigEntryAuthFailed("Incorrect Password")
except (ClientConnectionError, asyncio.TimeoutError) as ex:
raise ConfigEntryNotReady("Can not connect to host") from ex
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)

View File

@ -1,10 +1,14 @@
"""Config flow for Aladdin Connect cover integration.""" """Config flow for Aladdin Connect cover integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging import logging
from typing import Any from typing import Any
from aladdin_connect import AladdinConnectClient from AIOAladdinConnect import AladdinConnectClient
from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -12,6 +16,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
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.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
@ -32,8 +37,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
""" """
acc = AladdinConnectClient(data[CONF_USERNAME], data[CONF_PASSWORD]) acc = AladdinConnectClient(
login = await hass.async_add_executor_job(acc.login) data[CONF_USERNAME], data[CONF_PASSWORD], async_get_clientsession(hass)
)
login = await acc.login()
await acc.close()
if not login: if not login:
raise InvalidAuth raise InvalidAuth
@ -44,9 +52,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
entry: config_entries.ConfigEntry | None entry: config_entries.ConfigEntry | None
async def async_step_reauth( async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle re-authentication with Aladdin Connect.""" """Handle re-authentication with Aladdin Connect."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
@ -68,8 +74,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
await validate_input(self.hass, data) await validate_input(self.hass, data)
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except (ClientConnectionError, asyncio.TimeoutError, ClientError):
errors["base"] = "cannot_connect"
else: else:
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
@ -104,6 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except InvalidAuth: except InvalidAuth:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except (ClientConnectionError, asyncio.TimeoutError, ClientError):
errors["base"] = "cannot_connect"
else: else:
await self.async_set_unique_id( await self.async_set_unique_id(
user_input["username"].lower(), raise_on_progress=False user_input["username"].lower(), raise_on_progress=False

View File

@ -1,10 +1,11 @@
"""Platform for the Aladdin Connect cover component.""" """Platform for the Aladdin Connect cover component."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import logging import logging
from typing import Any, Final from typing import Any, Final
from aladdin_connect import AladdinConnectClient from AIOAladdinConnect import AladdinConnectClient
import voluptuous as vol import voluptuous as vol
from homeassistant.components.cover import ( from homeassistant.components.cover import (
@ -34,6 +35,7 @@ _LOGGER: Final = logging.getLogger(__name__)
PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
) )
SCAN_INTERVAL = timedelta(seconds=300)
async def async_setup_platform( async def async_setup_platform(
@ -62,14 +64,12 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Aladdin Connect platform.""" """Set up the Aladdin Connect platform."""
acc = hass.data[DOMAIN][config_entry.entry_id] acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id]
doors = await hass.async_add_executor_job(acc.get_doors) doors = await acc.get_doors()
if doors is None: if doors is None:
raise PlatformNotReady("Error from Aladdin Connect getting doors") raise PlatformNotReady("Error from Aladdin Connect getting doors")
async_add_entities( async_add_entities(
(AladdinDevice(acc, door) for door in doors), (AladdinDevice(acc, door, config_entry) for door in doors),
update_before_add=True,
) )
@ -79,27 +79,63 @@ class AladdinDevice(CoverEntity):
_attr_device_class = CoverDeviceClass.GARAGE _attr_device_class = CoverDeviceClass.GARAGE
_attr_supported_features = SUPPORTED_FEATURES _attr_supported_features = SUPPORTED_FEATURES
def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: def __init__(
self, acc: AladdinConnectClient, device: DoorDevice, entry: ConfigEntry
) -> None:
"""Initialize the Aladdin Connect cover.""" """Initialize the Aladdin Connect cover."""
self._acc = acc self._acc = acc
self._device_id = device["device_id"] self._device_id = device["device_id"]
self._number = device["door_number"] self._number = device["door_number"]
self._attr_name = device["name"] self._attr_name = device["name"]
self._attr_unique_id = f"{self._device_id}-{self._number}" self._attr_unique_id = f"{self._device_id}-{self._number}"
def close_cover(self, **kwargs: Any) -> None: async def async_added_to_hass(self) -> None:
"""Connect Aladdin Connect to the cloud."""
async def update_callback() -> None:
"""Schedule a state update."""
self.async_write_ha_state()
self._acc.register_callback(update_callback, self._number)
await self._acc.get_doors(self._number)
async def async_will_remove_from_hass(self) -> None:
"""Close Aladdin Connect before removing."""
await self._acc.close()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Issue close command to cover.""" """Issue close command to cover."""
self._acc.close_door(self._device_id, self._number) await self._acc.close_door(self._device_id, self._number)
def open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Issue open command to cover.""" """Issue open command to cover."""
self._acc.open_door(self._device_id, self._number) await self._acc.open_door(self._device_id, self._number)
def update(self) -> None: async def async_update(self) -> None:
"""Update status of cover.""" """Update status of cover."""
status = STATES_MAP.get( await self._acc.get_doors(self._number)
self._acc.get_door_status(self._device_id, self._number)
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
value = STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number))
if value is None:
return None
return value == STATE_CLOSED
@property
def is_closing(self) -> bool:
"""Update is closing attribute."""
return (
STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number))
== STATE_CLOSING
)
@property
def is_opening(self) -> bool:
"""Update is opening attribute."""
return (
STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number))
== STATE_OPENING
) )
self._attr_is_opening = status == STATE_OPENING
self._attr_is_closing = status == STATE_CLOSING
self._attr_is_closed = None if status is None else status == STATE_CLOSED

View File

@ -2,7 +2,7 @@
"domain": "aladdin_connect", "domain": "aladdin_connect",
"name": "Aladdin Connect", "name": "Aladdin Connect",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"requirements": ["aladdin_connect==0.4"], "requirements": ["AIOAladdinConnect==0.1.21"],
"codeowners": ["@mkmer"], "codeowners": ["@mkmer"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aladdin_connect"], "loggers": ["aladdin_connect"],

View File

@ -13,6 +13,7 @@
"data": { "data": {
"password": "Contrase\u00f1a" "password": "Contrase\u00f1a"
}, },
"description": "La integraci\u00f3n de Aladdin Connect necesita volver a autenticar su cuenta",
"title": "Reautenticaci\u00f3n de la integraci\u00f3n" "title": "Reautenticaci\u00f3n de la integraci\u00f3n"
}, },
"user": { "user": {

View File

@ -1,11 +1,11 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado",
"reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida"
}, },
"error": { "error": {
"cannot_connect": "Falhou ao conectar", "cannot_connect": "Falha ao conectar",
"invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida"
}, },
"step": { "step": {
@ -19,7 +19,7 @@
"user": { "user": {
"data": { "data": {
"password": "Senha", "password": "Senha",
"username": "Nome de usu\u00e1rio" "username": "Usu\u00e1rio"
} }
} }
} }

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
"reauth_successful": "\u00c5terautentisering lyckades"
},
"error": {
"cannot_connect": "Det gick inte att ansluta.",
"invalid_auth": "Ogiltig autentisering"
},
"step": {
"reauth_confirm": {
"data": {
"password": "L\u00f6senord"
},
"title": "\u00c5terautenticera integration"
},
"user": {
"data": {
"password": "L\u00f6senord",
"username": "Anv\u00e4ndarnamn"
}
}
}
}
}

View File

@ -7,6 +7,9 @@
"disarm": "Avlarma {entity_name}", "disarm": "Avlarma {entity_name}",
"trigger": "Utl\u00f6sare {entity_name}" "trigger": "Utl\u00f6sare {entity_name}"
}, },
"condition_type": {
"is_triggered": "har utl\u00f6sts"
},
"trigger_type": { "trigger_type": {
"armed_away": "{entity_name} larmad borta", "armed_away": "{entity_name} larmad borta",
"armed_home": "{entity_name} larmad hemma", "armed_home": "{entity_name} larmad hemma",

View File

@ -4,6 +4,7 @@
"arm_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", "arm_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212",
"arm_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", "arm_home": "{entity_name} \u5728\u5bb6\u8b66\u6212",
"arm_night": "{entity_name} \u591c\u95f4\u8b66\u6212", "arm_night": "{entity_name} \u591c\u95f4\u8b66\u6212",
"arm_vacation": "{entity_name} \u5ea6\u5047\u8b66\u6212",
"disarm": "\u89e3\u9664 {entity_name} \u8b66\u6212", "disarm": "\u89e3\u9664 {entity_name} \u8b66\u6212",
"trigger": "\u89e6\u53d1 {entity_name}" "trigger": "\u89e6\u53d1 {entity_name}"
}, },
@ -11,6 +12,7 @@
"is_armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", "is_armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212",
"is_armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", "is_armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212",
"is_armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", "is_armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212",
"is_armed_vacation": "{entity_name} \u5ea6\u5047\u8b66\u6212",
"is_disarmed": "{entity_name} \u8b66\u6212\u5df2\u89e3\u9664", "is_disarmed": "{entity_name} \u8b66\u6212\u5df2\u89e3\u9664",
"is_triggered": "{entity_name} \u8b66\u62a5\u5df2\u89e6\u53d1" "is_triggered": "{entity_name} \u8b66\u62a5\u5df2\u89e6\u53d1"
}, },
@ -18,6 +20,7 @@
"armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", "armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212",
"armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", "armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212",
"armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", "armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212",
"armed_vacation": "{entity_name} \u5ea6\u5047\u8b66\u6212",
"disarmed": "{entity_name} \u8b66\u6212\u89e3\u9664", "disarmed": "{entity_name} \u8b66\u6212\u89e3\u9664",
"triggered": "{entity_name} \u89e6\u53d1\u8b66\u62a5" "triggered": "{entity_name} \u89e6\u53d1\u8b66\u62a5"
} }

View File

@ -1,4 +1,6 @@
"""Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).""" """Support for AlarmDecoder-based alarm control panels (Honeywell/DSC)."""
from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
@ -91,7 +93,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
self._attr_code_arm_required = code_arm_required self._attr_code_arm_required = code_arm_required
self._alt_night_mode = alt_night_mode self._alt_night_mode = alt_night_mode
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
@ -126,12 +128,12 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
} }
self.schedule_update_ha_state() self.schedule_update_ha_state()
def alarm_disarm(self, code=None): def alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
if code: if code:
self._client.send(f"{code!s}1") self._client.send(f"{code!s}1")
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """Send arm away command."""
self._client.arm_away( self._client.arm_away(
code=code, code=code,
@ -139,7 +141,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
auto_bypass=self._auto_bypass, auto_bypass=self._auto_bypass,
) )
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """Send arm home command."""
self._client.arm_home( self._client.arm_home(
code=code, code=code,
@ -147,7 +149,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
auto_bypass=self._auto_bypass, auto_bypass=self._auto_bypass,
) )
def alarm_arm_night(self, code=None): def alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.""" """Send arm night command."""
self._client.arm_night( self._client.arm_night(
code=code, code=code,

View File

@ -1,4 +1,6 @@
"""Config flow for AlarmDecoder.""" """Config flow for AlarmDecoder."""
from __future__ import annotations
import logging import logging
from adext import AdExt from adext import AdExt
@ -58,7 +60,9 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> AlarmDecoderOptionsFlowHandler:
"""Get the options flow for AlarmDecoder.""" """Get the options flow for AlarmDecoder."""
return AlarmDecoderOptionsFlowHandler(config_entry) return AlarmDecoderOptionsFlowHandler(config_entry)

View File

@ -4,9 +4,11 @@ from __future__ import annotations
import logging import logging
from homeassistant.components import ( from homeassistant.components import (
button,
cover, cover,
fan, fan,
image_processing, image_processing,
input_button,
input_number, input_number,
light, light,
timer, timer,
@ -1044,6 +1046,8 @@ class AlexaThermostatController(AlexaCapability):
if preset in API_THERMOSTAT_PRESETS: if preset in API_THERMOSTAT_PRESETS:
mode = API_THERMOSTAT_PRESETS[preset] mode = API_THERMOSTAT_PRESETS[preset]
elif self.entity.state == STATE_UNKNOWN:
return None
else: else:
mode = API_THERMOSTAT_MODES.get(self.entity.state) mode = API_THERMOSTAT_MODES.get(self.entity.state)
if mode is None: if mode is None:
@ -1891,7 +1895,10 @@ class AlexaEventDetectionSensor(AlexaCapability):
if self.entity.domain == image_processing.DOMAIN: if self.entity.domain == image_processing.DOMAIN:
if int(state): if int(state):
human_presence = "DETECTED" human_presence = "DETECTED"
elif state == STATE_ON: elif state == STATE_ON or self.entity.domain in [
input_button.DOMAIN,
button.DOMAIN,
]:
human_presence = "DETECTED" human_presence = "DETECTED"
return {"value": human_presence} return {"value": human_presence}
@ -1903,7 +1910,8 @@ class AlexaEventDetectionSensor(AlexaCapability):
"detectionModes": { "detectionModes": {
"humanPresence": { "humanPresence": {
"featureAvailability": "ENABLED", "featureAvailability": "ENABLED",
"supportsNotDetected": True, "supportsNotDetected": self.entity.domain
not in [input_button.DOMAIN, button.DOMAIN],
} }
}, },
} }

View File

@ -382,7 +382,6 @@ def async_get_entities(hass, config) -> list[AlexaEntity]:
@ENTITY_ADAPTERS.register(alert.DOMAIN) @ENTITY_ADAPTERS.register(alert.DOMAIN)
@ENTITY_ADAPTERS.register(automation.DOMAIN) @ENTITY_ADAPTERS.register(automation.DOMAIN)
@ENTITY_ADAPTERS.register(group.DOMAIN) @ENTITY_ADAPTERS.register(group.DOMAIN)
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
class GenericCapabilities(AlexaEntity): class GenericCapabilities(AlexaEntity):
"""A generic, on/off device. """A generic, on/off device.
@ -405,12 +404,16 @@ class GenericCapabilities(AlexaEntity):
] ]
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
@ENTITY_ADAPTERS.register(switch.DOMAIN) @ENTITY_ADAPTERS.register(switch.DOMAIN)
class SwitchCapabilities(AlexaEntity): class SwitchCapabilities(AlexaEntity):
"""Class to represent Switch capabilities.""" """Class to represent Switch capabilities."""
def default_display_categories(self): def default_display_categories(self):
"""Return the display categories for this entity.""" """Return the display categories for this entity."""
if self.entity.domain == input_boolean.DOMAIN:
return [DisplayCategory.OTHER]
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
if device_class == switch.SwitchDeviceClass.OUTLET: if device_class == switch.SwitchDeviceClass.OUTLET:
return [DisplayCategory.SMARTPLUG] return [DisplayCategory.SMARTPLUG]
@ -421,6 +424,7 @@ class SwitchCapabilities(AlexaEntity):
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
return [ return [
AlexaPowerController(self.entity), AlexaPowerController(self.entity),
AlexaContactSensor(self.hass, self.entity),
AlexaEndpointHealth(self.hass, self.entity), AlexaEndpointHealth(self.hass, self.entity),
Alexa(self.hass), Alexa(self.hass),
] ]
@ -439,6 +443,8 @@ class ButtonCapabilities(AlexaEntity):
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
return [ return [
AlexaSceneController(self.entity, supports_deactivation=False), AlexaSceneController(self.entity, supports_deactivation=False),
AlexaEventDetectionSensor(self.hass, self.entity),
AlexaEndpointHealth(self.hass, self.entity),
Alexa(self.hass), Alexa(self.hass),
] ]

View File

@ -127,11 +127,6 @@ async def async_handle_message(hass, message):
@HANDLERS.register("SessionEndedRequest") @HANDLERS.register("SessionEndedRequest")
async def async_handle_session_end(hass, message):
"""Handle a session end request."""
return None
@HANDLERS.register("IntentRequest") @HANDLERS.register("IntentRequest")
@HANDLERS.register("LaunchRequest") @HANDLERS.register("LaunchRequest")
async def async_handle_intent(hass, message): async def async_handle_intent(hass, message):
@ -151,6 +146,11 @@ async def async_handle_intent(hass, message):
intent_name = ( intent_name = (
message.get("session", {}).get("application", {}).get("applicationId") message.get("session", {}).get("application", {}).get("applicationId")
) )
elif req["type"] == "SessionEndedRequest":
app_id = message.get("session", {}).get("application", {}).get("applicationId")
intent_name = f"{app_id}.{req['type']}"
alexa_response.variables["reason"] = req["reason"]
alexa_response.variables["error"] = req.get("error")
else: else:
intent_name = alexa_intent_info["name"] intent_name = alexa_intent_info["name"]

View File

@ -64,7 +64,7 @@ class AlmondFlowHandler(
"""Handle authorize step.""" """Handle authorize step."""
result = await super().async_step_auth(user_input) result = await super().async_step_auth(user_input)
if result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP: if result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP:
self.host = str(URL(result["url"]).with_path("me")) self.host = str(URL(result["url"]).with_path("me"))
return result return result

View File

@ -1,6 +1,7 @@
"""Config flow to configure the Ambee integration.""" """Config flow to configure the Ambee integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any from typing import Any
from ambee import Ambee, AmbeeAuthenticationError, AmbeeError from ambee import Ambee, AmbeeAuthenticationError, AmbeeError
@ -71,7 +72,7 @@ class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle initiation of re-authentication with Ambee.""" """Handle initiation of re-authentication with Ambee."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()

View File

@ -0,0 +1,16 @@
{
"config": {
"step": {
"reauth_confirm": {
"data": {
"api_key": "API-nyckel"
}
},
"user": {
"data": {
"api_key": "API-nyckel"
}
}
}
}
}

View File

@ -1,5 +1,8 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430"
},
"error": { "error": {
"invalid_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447 \u0438/\u0438\u043b\u0438 Application \u043a\u043b\u044e\u0447", "invalid_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447 \u0438/\u0438\u043b\u0438 Application \u043a\u043b\u044e\u0447",
"no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430" "no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430"

View File

@ -60,7 +60,7 @@ def get_androidtv_mac(dev_props: dict[str, Any]) -> str | None:
def _setup_androidtv( def _setup_androidtv(
hass: HomeAssistant, config: dict[str, Any] hass: HomeAssistant, config: Mapping[str, Any]
) -> tuple[str, PythonRSASigner | None, str]: ) -> tuple[str, PythonRSASigner | None, str]:
"""Generate an ADB key (if needed) and load it.""" """Generate an ADB key (if needed) and load it."""
adbkey: str = config.get( adbkey: str = config.get(

View File

@ -1,7 +1,6 @@
"""Rest API for Home Assistant.""" """Rest API for Home Assistant."""
import asyncio import asyncio
from http import HTTPStatus from http import HTTPStatus
import json
import logging import logging
from aiohttp import web from aiohttp import web
@ -29,7 +28,7 @@ import homeassistant.core as ha
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.json import json_dumps, json_loads
from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -108,7 +107,7 @@ class APIEventStream(HomeAssistantView):
if event.event_type == EVENT_HOMEASSISTANT_STOP: if event.event_type == EVENT_HOMEASSISTANT_STOP:
data = stop_obj data = stop_obj
else: else:
data = json.dumps(event, cls=JSONEncoder) data = json_dumps(event)
await to_write.put(data) await to_write.put(data)
@ -261,7 +260,7 @@ class APIEventView(HomeAssistantView):
raise Unauthorized() raise Unauthorized()
body = await request.text() body = await request.text()
try: try:
event_data = json.loads(body) if body else None event_data = json_loads(body) if body else None
except ValueError: except ValueError:
return self.json_message( return self.json_message(
"Event data should be valid JSON.", HTTPStatus.BAD_REQUEST "Event data should be valid JSON.", HTTPStatus.BAD_REQUEST
@ -314,7 +313,7 @@ class APIDomainServicesView(HomeAssistantView):
hass: ha.HomeAssistant = request.app["hass"] hass: ha.HomeAssistant = request.app["hass"]
body = await request.text() body = await request.text()
try: try:
data = json.loads(body) if body else None data = json_loads(body) if body else None
except ValueError: except ValueError:
return self.json_message( return self.json_message(
"Data should be valid JSON.", HTTPStatus.BAD_REQUEST "Data should be valid JSON.", HTTPStatus.BAD_REQUEST

View File

@ -23,6 +23,7 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -49,6 +50,13 @@ PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry for Apple TV.""" """Set up a config entry for Apple TV."""
manager = AppleTVManager(hass, entry) manager = AppleTVManager(hass, entry)
if manager.is_on:
await manager.connect_once(raise_missing_credentials=True)
if not manager.atv:
address = entry.data[CONF_ADDRESS]
raise ConfigEntryNotReady(f"Not found at {address}, waiting for discovery")
hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager
async def on_hass_stop(event): async def on_hass_stop(event):
@ -115,6 +123,10 @@ class AppleTVEntity(Entity):
self.atv = None self.atv = None
self.async_write_ha_state() self.async_write_ha_state()
if self.manager.atv:
# ATV is already connected
_async_connected(self.manager.atv)
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, f"{SIGNAL_CONNECTED}_{self.unique_id}", _async_connected self.hass, f"{SIGNAL_CONNECTED}_{self.unique_id}", _async_connected
@ -148,14 +160,14 @@ class AppleTVManager:
self.config_entry = config_entry self.config_entry = config_entry
self.hass = hass self.hass = hass
self.atv = None self.atv = None
self._is_on = not config_entry.options.get(CONF_START_OFF, False) self.is_on = not config_entry.options.get(CONF_START_OFF, False)
self._connection_attempts = 0 self._connection_attempts = 0
self._connection_was_lost = False self._connection_was_lost = False
self._task = None self._task = None
async def init(self): async def init(self):
"""Initialize power management.""" """Initialize power management."""
if self._is_on: if self.is_on:
await self.connect() await self.connect()
def connection_lost(self, _): def connection_lost(self, _):
@ -186,13 +198,13 @@ class AppleTVManager:
async def connect(self): async def connect(self):
"""Connect to device.""" """Connect to device."""
self._is_on = True self.is_on = True
self._start_connect_loop() self._start_connect_loop()
async def disconnect(self): async def disconnect(self):
"""Disconnect from device.""" """Disconnect from device."""
_LOGGER.debug("Disconnecting from device") _LOGGER.debug("Disconnecting from device")
self._is_on = False self.is_on = False
try: try:
if self.atv: if self.atv:
self.atv.close() self.atv.close()
@ -205,50 +217,53 @@ class AppleTVManager:
def _start_connect_loop(self): def _start_connect_loop(self):
"""Start background connect loop to device.""" """Start background connect loop to device."""
if not self._task and self.atv is None and self._is_on: if not self._task and self.atv is None and self.is_on:
self._task = asyncio.create_task(self._connect_loop()) self._task = asyncio.create_task(self._connect_loop())
else: else:
_LOGGER.debug( _LOGGER.debug(
"Not starting connect loop (%s, %s)", self.atv is None, self._is_on "Not starting connect loop (%s, %s)", self.atv is None, self.is_on
) )
async def connect_once(self, raise_missing_credentials):
"""Try to connect once."""
try:
if conf := await self._scan():
await self._connect(conf, raise_missing_credentials)
except exceptions.AuthenticationError:
self.config_entry.async_start_reauth(self.hass)
asyncio.create_task(self.disconnect())
_LOGGER.exception(
"Authentication failed for %s, try reconfiguring device",
self.config_entry.data[CONF_NAME],
)
return
except asyncio.CancelledError:
pass
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Failed to connect")
self.atv = None
async def _connect_loop(self): async def _connect_loop(self):
"""Connect loop background task function.""" """Connect loop background task function."""
_LOGGER.debug("Starting connect loop") _LOGGER.debug("Starting connect loop")
# Try to find device and connect as long as the user has said that # Try to find device and connect as long as the user has said that
# we are allowed to connect and we are not already connected. # we are allowed to connect and we are not already connected.
while self._is_on and self.atv is None: while self.is_on and self.atv is None:
try: await self.connect_once(raise_missing_credentials=False)
conf = await self._scan() if self.atv is not None:
if conf:
await self._connect(conf)
except exceptions.AuthenticationError:
self.config_entry.async_start_reauth(self.hass)
asyncio.create_task(self.disconnect())
_LOGGER.exception(
"Authentication failed for %s, try reconfiguring device",
self.config_entry.data[CONF_NAME],
)
break break
except asyncio.CancelledError: self._connection_attempts += 1
pass backoff = min(
except Exception: # pylint: disable=broad-except max(
_LOGGER.exception("Failed to connect") BACKOFF_TIME_LOWER_LIMIT,
self.atv = None randrange(2**self._connection_attempts),
),
BACKOFF_TIME_UPPER_LIMIT,
)
if self.atv is None: _LOGGER.debug("Reconnecting in %d seconds", backoff)
self._connection_attempts += 1 await asyncio.sleep(backoff)
backoff = min(
max(
BACKOFF_TIME_LOWER_LIMIT,
randrange(2**self._connection_attempts),
),
BACKOFF_TIME_UPPER_LIMIT,
)
_LOGGER.debug("Reconnecting in %d seconds", backoff)
await asyncio.sleep(backoff)
_LOGGER.debug("Connect loop ended") _LOGGER.debug("Connect loop ended")
self._task = None self._task = None
@ -287,23 +302,33 @@ class AppleTVManager:
# it will update the address and reload the config entry when the device is found. # it will update the address and reload the config entry when the device is found.
return None return None
async def _connect(self, conf): async def _connect(self, conf, raise_missing_credentials):
"""Connect to device.""" """Connect to device."""
credentials = self.config_entry.data[CONF_CREDENTIALS] credentials = self.config_entry.data[CONF_CREDENTIALS]
session = async_get_clientsession(self.hass) name = self.config_entry.data[CONF_NAME]
missing_protocols = []
for protocol_int, creds in credentials.items(): for protocol_int, creds in credentials.items():
protocol = Protocol(int(protocol_int)) protocol = Protocol(int(protocol_int))
if conf.get_service(protocol) is not None: if conf.get_service(protocol) is not None:
conf.set_credentials(protocol, creds) conf.set_credentials(protocol, creds)
else: else:
_LOGGER.warning( missing_protocols.append(protocol.name)
"Protocol %s not found for %s, functionality will be reduced",
protocol.name, if missing_protocols:
self.config_entry.data[CONF_NAME], missing_protocols_str = ", ".join(missing_protocols)
if raise_missing_credentials:
raise ConfigEntryNotReady(
f"Protocol(s) {missing_protocols_str} not yet found for {name}, waiting for discovery."
) )
_LOGGER.info(
"Protocol(s) %s not yet found for %s, trying later",
missing_protocols_str,
name,
)
return
_LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME]) _LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME])
session = async_get_clientsession(self.hass)
self.atv = await connect(conf, self.hass.loop, session=session) self.atv = await connect(conf, self.hass.loop, session=session)
self.atv.listener = self self.atv.listener = self

View File

@ -21,8 +21,8 @@ def build_app_list(app_list):
media_content_id="apps", media_content_id="apps",
media_content_type=MEDIA_TYPE_APPS, media_content_type=MEDIA_TYPE_APPS,
title="Apps", title="Apps",
can_play=True, can_play=False,
can_expand=False, can_expand=True,
children=[item_payload(item) for item in app_list], children=[item_payload(item) for item in app_list],
children_media_class=MEDIA_CLASS_APP, children_media_class=MEDIA_CLASS_APP,
) )

View File

@ -3,9 +3,11 @@ from __future__ import annotations
import asyncio import asyncio
from collections import deque from collections import deque
from collections.abc import Mapping
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging
from random import randrange from random import randrange
from typing import Any
from pyatv import exceptions, pair, scan from pyatv import exceptions, pair, scan
from pyatv.const import DeviceModel, PairingRequirement, Protocol from pyatv.const import DeviceModel, PairingRequirement, Protocol
@ -13,10 +15,11 @@ from pyatv.convert import model_str, protocol_str
from pyatv.helpers import get_unique_id from pyatv.helpers import get_unique_id
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.network import is_ipv6_address from homeassistant.util.network import is_ipv6_address
@ -71,7 +74,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> AppleTVOptionsFlow:
"""Get options flow for this handler.""" """Get options flow for this handler."""
return AppleTVOptionsFlow(config_entry) return AppleTVOptionsFlow(config_entry)
@ -116,10 +121,10 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return entry.unique_id return entry.unique_id
return None return None
async def async_step_reauth(self, user_input=None): async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle initial step when updating invalid credentials.""" """Handle initial step when updating invalid credentials."""
self.context["title_placeholders"] = { self.context["title_placeholders"] = {
"name": user_input[CONF_NAME], "name": entry_data[CONF_NAME],
"type": "Apple TV", "type": "Apple TV",
} }
self.scan_filter = self.unique_id self.scan_filter = self.unique_id
@ -164,7 +169,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_zeroconf( async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> data_entry_flow.FlowResult: ) -> FlowResult:
"""Handle device found via zeroconf.""" """Handle device found via zeroconf."""
host = discovery_info.host host = discovery_info.host
if is_ipv6_address(host): if is_ipv6_address(host):
@ -248,7 +253,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
): ):
# Add potentially new identifiers from this device to the existing flow # Add potentially new identifiers from this device to the existing flow
context["all_identifiers"].append(unique_id) context["all_identifiers"].append(unique_id)
raise data_entry_flow.AbortFlow("already_in_progress") raise AbortFlow("already_in_progress")
async def async_found_zeroconf_device(self, user_input=None): async def async_found_zeroconf_device(self, user_input=None):
"""Handle device found after Zeroconf discovery.""" """Handle device found after Zeroconf discovery."""
@ -523,7 +528,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class AppleTVOptionsFlow(config_entries.OptionsFlow): class AppleTVOptionsFlow(config_entries.OptionsFlow):
"""Handle Apple TV options.""" """Handle Apple TV options."""
def __init__(self, config_entry): def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize Apple TV options flow.""" """Initialize Apple TV options flow."""
self.config_entry = config_entry self.config_entry = config_entry
self.options = dict(config_entry.options) self.options = dict(config_entry.options)

View File

@ -3,7 +3,7 @@
"name": "Apple TV", "name": "Apple TV",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/apple_tv", "documentation": "https://www.home-assistant.io/integrations/apple_tv",
"requirements": ["pyatv==0.10.0"], "requirements": ["pyatv==0.10.2"],
"dependencies": ["zeroconf"], "dependencies": ["zeroconf"],
"zeroconf": [ "zeroconf": [
"_mediaremotetv._tcp.local.", "_mediaremotetv._tcp.local.",

View File

@ -282,22 +282,20 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
# RAOP. Otherwise try to play it with regular AirPlay. # RAOP. Otherwise try to play it with regular AirPlay.
if media_type == MEDIA_TYPE_APP: if media_type == MEDIA_TYPE_APP:
await self.atv.apps.launch_app(media_id) await self.atv.apps.launch_app(media_id)
return
if media_source.is_media_source_id(media_id): if media_source.is_media_source_id(media_id):
play_item = await media_source.async_resolve_media( play_item = await media_source.async_resolve_media(
self.hass, media_id, self.entity_id self.hass, media_id, self.entity_id
) )
media_id = play_item.url media_id = async_process_play_media_url(self.hass, play_item.url)
media_type = MEDIA_TYPE_MUSIC media_type = MEDIA_TYPE_MUSIC
media_id = async_process_play_media_url(self.hass, media_id)
if self._is_feature_available(FeatureName.StreamFile) and ( if self._is_feature_available(FeatureName.StreamFile) and (
media_type == MEDIA_TYPE_MUSIC or await is_streamable(media_id) media_type == MEDIA_TYPE_MUSIC or await is_streamable(media_id)
): ):
_LOGGER.debug("Streaming %s via RAOP", media_id) _LOGGER.debug("Streaming %s via RAOP", media_id)
await self.atv.stream.stream_file(media_id) await self.atv.stream.stream_file(media_id)
elif self._is_feature_available(FeatureName.PlayUrl): elif self._is_feature_available(FeatureName.PlayUrl):
_LOGGER.debug("Playing %s via AirPlay", media_id) _LOGGER.debug("Playing %s via AirPlay", media_id)
await self.atv.stream.play_url(media_id) await self.atv.stream.play_url(media_id)

View File

@ -7,6 +7,7 @@
"device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.",
"device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.", "device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.",
"inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con el DNS de multidifusi\u00f3n (Zeroconf). Por favor, intente a\u00f1adir el dispositivo de nuevo.", "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con el DNS de multidifusi\u00f3n (Zeroconf). Por favor, intente a\u00f1adir el dispositivo de nuevo.",
"ipv6_not_supported": "IPv6 no est\u00e1 soportado.",
"no_devices_found": "No se encontraron dispositivos en la red", "no_devices_found": "No se encontraron dispositivos en la red",
"reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente",
"setup_failed": "No se ha podido configurar el dispositivo.", "setup_failed": "No se ha podido configurar el dispositivo.",

View File

@ -253,6 +253,11 @@ class ApplicationCredentialsProtocol(Protocol):
) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: ) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
"""Return a custom auth implementation.""" """Return a custom auth implementation."""
async def async_get_description_placeholders(
self, hass: HomeAssistant
) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
async def _get_platform( async def _get_platform(
hass: HomeAssistant, integration_domain: str hass: HomeAssistant, integration_domain: str
@ -282,6 +287,14 @@ async def _get_platform(
return platform return platform
async def _async_integration_config(hass: HomeAssistant, domain: str) -> dict[str, Any]:
platform = await _get_platform(hass, domain)
if platform and hasattr(platform, "async_get_description_placeholders"):
placeholders = await platform.async_get_description_placeholders(hass)
return {"description_placeholders": placeholders}
return {}
@websocket_api.websocket_command( @websocket_api.websocket_command(
{vol.Required("type"): "application_credentials/config"} {vol.Required("type"): "application_credentials/config"}
) )
@ -290,6 +303,11 @@ async def handle_integration_list(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None: ) -> None:
"""Handle integrations command.""" """Handle integrations command."""
connection.send_result( domains = await async_get_application_credentials(hass)
msg["id"], {"domains": await async_get_application_credentials(hass)} result = {
) "domains": domains,
"integrations": {
domain: await _async_integration_config(hass, domain) for domain in domains
},
}
connection.send_result(msg["id"], result)

View File

@ -2,7 +2,7 @@
"domain": "apprise", "domain": "apprise",
"name": "Apprise", "name": "Apprise",
"documentation": "https://www.home-assistant.io/integrations/apprise", "documentation": "https://www.home-assistant.io/integrations/apprise",
"requirements": ["apprise==0.9.8.3"], "requirements": ["apprise==0.9.9"],
"codeowners": ["@caronc"], "codeowners": ["@caronc"],
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["apprise"] "loggers": ["apprise"]

View File

@ -3,5 +3,10 @@
"abort": { "abort": {
"cannot_connect": "\u8fde\u63a5\u5931\u8d25" "cannot_connect": "\u8fde\u63a5\u5931\u8d25"
} }
},
"device_automation": {
"trigger_type": {
"turn_on": "{entity_name} \u88ab\u8981\u6c42\u6253\u5f00"
}
} }
} }

View File

@ -1,11 +1,13 @@
{ {
"config": { "config": {
"error": { "error": {
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435",
"unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
}, },
"step": { "step": {
"user": { "user": {
"data": { "data": {
"mode": "\u0420\u0435\u0436\u0438\u043c",
"name": "\u0418\u043c\u0435", "name": "\u0418\u043c\u0435",
"password": "\u041f\u0430\u0440\u043e\u043b\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
"port": "\u041f\u043e\u0440\u0442", "port": "\u041f\u043e\u0440\u0442",

View File

@ -1,7 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo" "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo",
"no_unique_id": "Un dispositivo sin una identificaci\u00f3n \u00fanica v\u00e1lida ya est\u00e1 configurado. La configuraci\u00f3n de una instancia m\u00faltiple no es posible"
}, },
"error": { "error": {
"cannot_connect": "No se pudo conectar", "cannot_connect": "No se pudo conectar",

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"invalid_unique_id": "Imposs\u00edvel determinar um ID exclusivo v\u00e1lido para o dispositivo", "invalid_unique_id": "Imposs\u00edvel determinar um ID exclusivo v\u00e1lido para o dispositivo",
"no_unique_id": "[%key:component::asuswrt::config::abort::not_unique_id_exist%]" "no_unique_id": "Um dispositivo sem um ID exclusivo v\u00e1lido j\u00e1 est\u00e1 configurado. A configura\u00e7\u00e3o de v\u00e1rias inst\u00e2ncias n\u00e3o \u00e9 poss\u00edvel"
}, },
"error": { "error": {
"cannot_connect": "Falha ao conectar", "cannot_connect": "Falha ao conectar",

View File

@ -31,7 +31,7 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"consider_home": "Sekunder att v\u00e4nta tills attt en enhet anses borta", "consider_home": "Sekunder att v\u00e4nta tills att en enhet anses borta",
"dnsmasq": "Platsen i routern f\u00f6r dnsmasq.leases-filerna", "dnsmasq": "Platsen i routern f\u00f6r dnsmasq.leases-filerna",
"interface": "Gr\u00e4nssnittet som du vill ha statistik fr\u00e5n (t.ex. eth0, eth1 etc)", "interface": "Gr\u00e4nssnittet som du vill ha statistik fr\u00e5n (t.ex. eth0, eth1 etc)",
"require_ip": "Enheterna m\u00e5ste ha IP (f\u00f6r accesspunktsl\u00e4ge)", "require_ip": "Enheterna m\u00e5ste ha IP (f\u00f6r accesspunktsl\u00e4ge)",

View File

@ -3,6 +3,9 @@
"abort": { "abort": {
"already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e"
}, },
"error": {
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435"
},
"step": { "step": {
"user": { "user": {
"data": { "data": {

View File

@ -21,6 +21,7 @@ from homeassistant.exceptions import (
ConfigEntryNotReady, ConfigEntryNotReady,
HomeAssistantError, HomeAssistantError,
) )
from homeassistant.helpers import device_registry as dr
from .activity import ActivityStream from .activity import ActivityStream
from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS
@ -283,12 +284,15 @@ class AugustData(AugustSubscriberMixin):
device.device_id, device.device_id,
) )
def _get_device_name(self, device_id): def get_device(self, device_id: str) -> Doorbell | Lock | None:
"""Get a device by id."""
return self._locks_by_id.get(device_id) or self._doorbells_by_id.get(device_id)
def _get_device_name(self, device_id: str) -> str | None:
"""Return doorbell or lock name as August has it stored.""" """Return doorbell or lock name as August has it stored."""
if device_id in self._locks_by_id: if device := self.get_device(device_id):
return self._locks_by_id[device_id].device_name return device.device_name
if device_id in self._doorbells_by_id: return None
return self._doorbells_by_id[device_id].device_name
async def async_lock(self, device_id): async def async_lock(self, device_id):
"""Lock the device.""" """Lock the device."""
@ -403,3 +407,15 @@ def _restore_live_attrs(lock_detail, attrs):
"""Restore the non-cache attributes after a cached update.""" """Restore the non-cache attributes after a cached update."""
for attr, value in attrs.items(): for attr, value in attrs.items():
setattr(lock_detail, attr, value) setattr(lock_detail, attr, value)
async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
) -> bool:
"""Remove august config entry from a device if its no longer present."""
data: AugustData = hass.data[DOMAIN][config_entry.entry_id]
return not any(
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN and data.get_device(identifier[1])
)

View File

@ -30,7 +30,7 @@ class AugustWakeLockButton(AugustEntityMixin, ButtonEntity):
self._attr_name = f"{device.device_name} Wake" self._attr_name = f"{device.device_name} Wake"
self._attr_unique_id = f"{self._device_id}_wake" self._attr_unique_id = f"{self._device_id}_wake"
async def async_press(self, **kwargs): async def async_press(self) -> None:
"""Wake the device.""" """Wake the device."""
await self._data.async_status_async(self._device_id, self._hyper_bridge) await self._data.async_status_async(self._device_id, self._hyper_bridge)

View File

@ -1,11 +1,14 @@
"""Config flow for August integration.""" """Config flow for August integration."""
from collections.abc import Mapping
import logging import logging
from typing import Any
import voluptuous as vol import voluptuous as vol
from yalexs.authenticator import ValidationResult from yalexs.authenticator import ValidationResult
from homeassistant import config_entries from homeassistant import config_entries
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_LOGIN_METHOD, DOMAIN, LOGIN_METHODS, VERIFICATION_CODE_KEY from .const import CONF_LOGIN_METHOD, DOMAIN, LOGIN_METHODS, VERIFICATION_CODE_KEY
from .exceptions import CannotConnect, InvalidAuth, RequireValidation from .exceptions import CannotConnect, InvalidAuth, RequireValidation
@ -109,9 +112,9 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}, },
) )
async def async_step_reauth(self, data): async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle configuration by re-auth.""" """Handle configuration by re-auth."""
self._user_auth_details = dict(data) self._user_auth_details = dict(entry_data)
self._mode = "reauth" self._mode = "reauth"
self._needs_reset = True self._needs_reset = True
self._august_gateway = AugustGateway(self.hass) self._august_gateway = AugustGateway(self.hass)

View File

@ -1,5 +1,6 @@
"""Support for August lock.""" """Support for August lock."""
import logging import logging
from typing import Any
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from yalexs.activity import SOURCE_PUBNUB, ActivityType from yalexs.activity import SOURCE_PUBNUB, ActivityType
@ -44,14 +45,14 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
self._attr_unique_id = f"{self._device_id:s}_lock" self._attr_unique_id = f"{self._device_id:s}_lock"
self._update_from_data() self._update_from_data()
async def async_lock(self, **kwargs): async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device.""" """Lock the device."""
if self._data.activity_stream.pubnub.connected: if self._data.activity_stream.pubnub.connected:
await self._data.async_lock_async(self._device_id, self._hyper_bridge) await self._data.async_lock_async(self._device_id, self._hyper_bridge)
return return
await self._call_lock_operation(self._data.async_lock) await self._call_lock_operation(self._data.async_lock)
async def async_unlock(self, **kwargs): async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device.""" """Unlock the device."""
if self._data.activity_stream.pubnub.connected: if self._data.activity_stream.pubnub.connected:
await self._data.async_unlock_async(self._device_id, self._hyper_bridge) await self._data.async_unlock_async(self._device_id, self._hyper_bridge)
@ -126,7 +127,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
"keypad_battery_level" "keypad_battery_level"
] = self._detail.keypad.battery_level ] = self._detail.keypad.battery_level
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log."""
await super().async_added_to_hass() await super().async_added_to_hass()

View File

@ -0,0 +1,14 @@
{
"config": {
"error": {
"invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435"
},
"step": {
"user_validate": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u0430"
}
}
}
}
}

View File

@ -1,4 +1,6 @@
"""Config flow for SpaceX Launches and Starman.""" """Config flow for SpaceX Launches and Starman."""
from __future__ import annotations
import logging import logging
from aiohttp import ClientError from aiohttp import ClientError
@ -22,7 +24,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler(config_entry) return OptionsFlowHandler(config_entry)
@ -82,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow changes.""" """Handle options flow changes."""
def __init__(self, config_entry): def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry self.config_entry = config_entry

View File

@ -17,7 +17,7 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"threshold": "\u3057\u304d\u3044\u5024(%)" "threshold": "\u95be\u5024(%)"
} }
} }
} }

View File

@ -10,15 +10,13 @@
import logging import logging
from aurorapy.client import AuroraError, AuroraSerialClient from aurorapy.client import AuroraSerialClient
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .config_flow import validate_and_connect from .const import DOMAIN
from .const import ATTR_SERIAL_NUMBER, DOMAIN
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
@ -31,42 +29,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
comport = entry.data[CONF_PORT] comport = entry.data[CONF_PORT]
address = entry.data[CONF_ADDRESS] address = entry.data[CONF_ADDRESS]
ser_client = AuroraSerialClient(address, comport, parity="N", timeout=1) ser_client = AuroraSerialClient(address, comport, parity="N", timeout=1)
# To handle yaml import attempts in darkness, (re)try connecting only if
# unique_id not yet assigned.
if entry.unique_id is None:
try:
res = await hass.async_add_executor_job(
validate_and_connect, hass, entry.data
)
except AuroraError as error:
if "No response after" in str(error):
raise ConfigEntryNotReady("No response (could be dark)") from error
_LOGGER.error("Failed to connect to inverter: %s", error)
return False
except OSError as error:
if error.errno == 19: # No such device.
_LOGGER.error("Failed to connect to inverter: no such COM port")
return False
_LOGGER.error("Failed to connect to inverter: %s", error)
return False
else:
# If we got here, the device is now communicating (maybe after
# being in darkness). But there's a small risk that the user has
# configured via the UI since we last attempted the yaml setup,
# which means we'd get a duplicate unique ID.
new_id = res[ATTR_SERIAL_NUMBER]
# Check if this unique_id has already been used
for existing_entry in hass.config_entries.async_entries(DOMAIN):
if existing_entry.unique_id == new_id:
_LOGGER.debug(
"Remove already configured config entry for id %s", new_id
)
hass.async_create_task(
hass.config_entries.async_remove(entry.entry_id)
)
return False
hass.config_entries.async_update_entry(entry, unique_id=new_id)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ser_client hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ser_client
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)

View File

@ -3,7 +3,7 @@
"name": "Aurora ABB PowerOne Solar PV", "name": "Aurora ABB PowerOne Solar PV",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone", "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone",
"requirements": ["aurorapy==0.2.6"], "requirements": ["aurorapy==0.2.7"],
"codeowners": ["@davet2001"], "codeowners": ["@davet2001"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aurorapy"] "loggers": ["aurorapy"]

View File

@ -5,7 +5,7 @@ from collections.abc import Mapping
import logging import logging
from typing import Any from typing import Any
from aurorapy.client import AuroraError, AuroraSerialClient from aurorapy.client import AuroraError, AuroraSerialClient, AuroraTimeoutError
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -102,22 +102,16 @@ class AuroraSensor(AuroraEntity, SensorEntity):
self._attr_native_value = round(energy_wh / 1000, 2) self._attr_native_value = round(energy_wh / 1000, 2)
self._attr_available = True self._attr_available = True
except AuroraTimeoutError:
self._attr_state = None
self._attr_native_value = None
self._attr_available = False
_LOGGER.debug("No response from inverter (could be dark)")
except AuroraError as error: except AuroraError as error:
self._attr_state = None self._attr_state = None
self._attr_native_value = None self._attr_native_value = None
self._attr_available = False self._attr_available = False
# aurorapy does not have different exceptions (yet) for dealing raise error
# with timeout vs other comms errors.
# This means the (normal) situation of no response during darkness
# raises an exception.
# aurorapy (gitlab) pull request merged 29/5/2019. When >0.2.6 is
# released, this could be modified to :
# except AuroraTimeoutError as e:
# Workaround: look at the text of the exception
if "No response after" in str(error):
_LOGGER.debug("No response from inverter (could be dark)")
else:
raise error
finally: finally:
if self._attr_available != self.available_prev: if self._attr_available != self.available_prev:
if self._attr_available: if self._attr_available:

View File

@ -0,0 +1,17 @@
{
"config": {
"abort": {
"no_serial_ports": "Inga com portar funna. M\u00e5ste ha en RS485 enhet f\u00f6r att kommunicera"
},
"error": {
"cannot_open_serial_port": "Kan inte \u00f6ppna serieporten, kontrollera och f\u00f6rs\u00f6k igen."
},
"step": {
"user": {
"data": {
"port": "RS485 eller USB-RS485 adapter port"
}
}
}
}
}

View File

@ -1,6 +1,7 @@
"""Config flow for Aussie Broadband integration.""" """Config flow for Aussie Broadband integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any from typing import Any
from aiohttp import ClientError from aiohttp import ClientError
@ -76,9 +77,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_reauth(self, user_input: dict[str, str]) -> FlowResult: async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle reauth on credential failure.""" """Handle reauth on credential failure."""
self._reauth_username = user_input[CONF_USERNAME] self._reauth_username = entry_data[CONF_USERNAME]
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()

View File

@ -1,12 +1,14 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "La cuenta ya est\u00e1 configurada",
"no_services_found": "No se han encontrado servicios para esta cuenta", "no_services_found": "No se han encontrado servicios para esta cuenta",
"reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente"
}, },
"error": { "error": {
"cannot_connect": "Fall\u00f3 la conexi\u00f3n", "cannot_connect": "Fall\u00f3 la conexi\u00f3n",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado"
}, },
"step": { "step": {
"reauth_confirm": { "reauth_confirm": {
@ -31,8 +33,16 @@
} }
}, },
"options": { "options": {
"abort": {
"cannot_connect": "Fallo en la conexi\u00f3n",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado"
},
"step": { "step": {
"init": { "init": {
"data": {
"services": "Servicios"
},
"title": "Selecciona servicios" "title": "Selecciona servicios"
} }
} }

View File

@ -150,44 +150,6 @@ from homeassistant.util import dt as dt_util
from . import indieauth, login_flow, mfa_setup_flow from . import indieauth, login_flow, mfa_setup_flow
DOMAIN = "auth" DOMAIN = "auth"
WS_TYPE_CURRENT_USER = "auth/current_user"
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_CURRENT_USER}
)
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token"
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
vol.Required("lifespan"): int, # days
vol.Required("client_name"): str,
vol.Optional("client_icon"): str,
}
)
WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens"
SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_REFRESH_TOKENS}
)
WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token"
SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN,
vol.Required("refresh_token_id"): str,
}
)
WS_TYPE_SIGN_PATH = "auth/sign_path"
SCHEMA_WS_SIGN_PATH = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{
vol.Required("type"): WS_TYPE_SIGN_PATH,
vol.Required("path"): str,
vol.Optional("expires", default=30): int,
}
)
RESULT_TYPE_CREDENTIALS = "credentials"
@bind_hass @bind_hass
@ -206,27 +168,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.http.register_view(LinkUserView(retrieve_result)) hass.http.register_view(LinkUserView(retrieve_result))
hass.http.register_view(OAuth2AuthorizeCallbackView()) hass.http.register_view(OAuth2AuthorizeCallbackView())
websocket_api.async_register_command( websocket_api.async_register_command(hass, websocket_current_user)
hass, WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER websocket_api.async_register_command(hass, websocket_create_long_lived_access_token)
) websocket_api.async_register_command(hass, websocket_refresh_tokens)
websocket_api.async_register_command( websocket_api.async_register_command(hass, websocket_delete_refresh_token)
hass, websocket_api.async_register_command(hass, websocket_sign_path)
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
websocket_create_long_lived_access_token,
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN,
)
websocket_api.async_register_command(
hass, WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS
)
websocket_api.async_register_command(
hass,
WS_TYPE_DELETE_REFRESH_TOKEN,
websocket_delete_refresh_token,
SCHEMA_WS_DELETE_REFRESH_TOKEN,
)
websocket_api.async_register_command(
hass, WS_TYPE_SIGN_PATH, websocket_sign_path, SCHEMA_WS_SIGN_PATH
)
await login_flow.async_setup(hass, store_result) await login_flow.async_setup(hass, store_result)
await mfa_setup_flow.async_setup(hass) await mfa_setup_flow.async_setup(hass)
@ -487,6 +433,7 @@ def _create_auth_code_store():
return store_result, retrieve_result return store_result, retrieve_result
@websocket_api.websocket_command({vol.Required("type"): "auth/current_user"})
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@websocket_api.async_response @websocket_api.async_response
async def websocket_current_user( async def websocket_current_user(
@ -524,6 +471,14 @@ async def websocket_current_user(
) )
@websocket_api.websocket_command(
{
vol.Required("type"): "auth/long_lived_access_token",
vol.Required("lifespan"): int, # days
vol.Required("client_name"): str,
vol.Optional("client_icon"): str,
}
)
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@websocket_api.async_response @websocket_api.async_response
async def websocket_create_long_lived_access_token( async def websocket_create_long_lived_access_token(
@ -541,13 +496,13 @@ async def websocket_create_long_lived_access_token(
try: try:
access_token = hass.auth.async_create_access_token(refresh_token) access_token = hass.auth.async_create_access_token(refresh_token)
except InvalidAuthError as exc: except InvalidAuthError as exc:
return websocket_api.error_message( connection.send_error(msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc))
msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc) return
)
connection.send_message(websocket_api.result_message(msg["id"], access_token)) connection.send_result(msg["id"], access_token)
@websocket_api.websocket_command({vol.Required("type"): "auth/refresh_tokens"})
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@callback @callback
def websocket_refresh_tokens( def websocket_refresh_tokens(
@ -555,27 +510,38 @@ def websocket_refresh_tokens(
): ):
"""Return metadata of users refresh tokens.""" """Return metadata of users refresh tokens."""
current_id = connection.refresh_token_id current_id = connection.refresh_token_id
connection.send_message(
websocket_api.result_message( tokens = []
msg["id"], for refresh in connection.user.refresh_tokens.values():
[ if refresh.credential:
{ auth_provider_type = refresh.credential.auth_provider_type
"id": refresh.id, else:
"client_id": refresh.client_id, auth_provider_type = None
"client_name": refresh.client_name,
"client_icon": refresh.client_icon, tokens.append(
"type": refresh.token_type, {
"created_at": refresh.created_at, "id": refresh.id,
"is_current": refresh.id == current_id, "client_id": refresh.client_id,
"last_used_at": refresh.last_used_at, "client_name": refresh.client_name,
"last_used_ip": refresh.last_used_ip, "client_icon": refresh.client_icon,
} "type": refresh.token_type,
for refresh in connection.user.refresh_tokens.values() "created_at": refresh.created_at,
], "is_current": refresh.id == current_id,
"last_used_at": refresh.last_used_at,
"last_used_ip": refresh.last_used_ip,
"auth_provider_type": auth_provider_type,
}
) )
)
connection.send_result(msg["id"], tokens)
@websocket_api.websocket_command(
{
vol.Required("type"): "auth/delete_refresh_token",
vol.Required("refresh_token_id"): str,
}
)
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@websocket_api.async_response @websocket_api.async_response
async def websocket_delete_refresh_token( async def websocket_delete_refresh_token(
@ -585,15 +551,21 @@ async def websocket_delete_refresh_token(
refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"]) refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"])
if refresh_token is None: if refresh_token is None:
return websocket_api.error_message( connection.send_error(msg["id"], "invalid_token_id", "Received invalid token")
msg["id"], "invalid_token_id", "Received invalid token" return
)
await hass.auth.async_remove_refresh_token(refresh_token) await hass.auth.async_remove_refresh_token(refresh_token)
connection.send_message(websocket_api.result_message(msg["id"], {})) connection.send_result(msg["id"], {})
@websocket_api.websocket_command(
{
vol.Required("type"): "auth/sign_path",
vol.Required("path"): str,
vol.Optional("expires", default=30): int,
}
)
@websocket_api.ws_require_user() @websocket_api.ws_require_user()
@callback @callback
def websocket_sign_path( def websocket_sign_path(

View File

@ -52,7 +52,7 @@ flow for details.
Progress the flow. Most flows will be 1 page, but could optionally add extra Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will login challenges, like TFA. Once the flow has finished, the returned step will
have type RESULT_TYPE_CREATE_ENTRY and "result" key will contain an authorization code. have type FlowResultType.CREATE_ENTRY and "result" key will contain an authorization code.
The authorization code associated with an authorized user by default, it will The authorization code associated with an authorized user by default, it will
associate with an credential if "type" set to "link_user" in associate with an credential if "type" set to "link_user" in
"/auth/login_flow" "/auth/login_flow"
@ -123,13 +123,13 @@ class AuthProvidersView(HomeAssistantView):
def _prepare_result_json(result): def _prepare_result_json(result):
"""Convert result to JSON.""" """Convert result to JSON."""
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
data = result.copy() data = result.copy()
data.pop("result") data.pop("result")
data.pop("data") data.pop("data")
return data return data
if result["type"] != data_entry_flow.RESULT_TYPE_FORM: if result["type"] != data_entry_flow.FlowResultType.FORM:
return result return result
data = result.copy() data = result.copy()
@ -154,11 +154,11 @@ class LoginFlowBaseView(HomeAssistantView):
async def _async_flow_result_to_response(self, request, client_id, result): async def _async_flow_result_to_response(self, request, client_id, result):
"""Convert the flow result to a response.""" """Convert the flow result to a response."""
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200. # @log_invalid_auth does not work here since it returns HTTP 200.
# We need to manually log failed login attempts. # We need to manually log failed login attempts.
if ( if (
result["type"] == data_entry_flow.RESULT_TYPE_FORM result["type"] == data_entry_flow.FlowResultType.FORM
and (errors := result.get("errors")) and (errors := result.get("errors"))
and errors.get("base") and errors.get("base")
in ( in (

View File

@ -129,11 +129,11 @@ def websocket_depose_mfa(
def _prepare_result_json(result): def _prepare_result_json(result):
"""Convert result to JSON.""" """Convert result to JSON."""
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
data = result.copy() data = result.copy()
return data return data
if result["type"] != data_entry_flow.RESULT_TYPE_FORM: if result["type"] != data_entry_flow.FlowResultType.FORM:
return result return result
data = result.copy() data = result.copy()

View File

@ -1,12 +1,16 @@
"""Config flow for Awair.""" """Config flow for Awair."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from python_awair import Awair from python_awair import Awair
from python_awair.exceptions import AuthError, AwairError from python_awair.exceptions import AuthError, AwairError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
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, LOGGER from .const import DOMAIN, LOGGER
@ -17,7 +21,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
async def async_step_user(self, user_input: dict | None = None): async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors = {} errors = {}
@ -42,8 +48,14 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_reauth(self, user_input: dict | None = None): async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle re-auth if token invalid.""" """Handle re-auth if token invalid."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm reauth dialog."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
@ -62,7 +74,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN):
errors = {CONF_ACCESS_TOKEN: error} errors = {CONF_ACCESS_TOKEN: error}
return self.async_show_form( return self.async_show_form(
step_id="reauth", step_id="reauth_confirm",
data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}), data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}),
errors=errors, errors=errors,
) )

View File

@ -8,7 +8,7 @@
"email": "[%key:common::config_flow::data::email%]" "email": "[%key:common::config_flow::data::email%]"
} }
}, },
"reauth": { "reauth_confirm": {
"description": "Please re-enter your Awair developer access token.", "description": "Please re-enter your Awair developer access token.",
"data": { "data": {
"access_token": "[%key:common::config_flow::data::access_token%]", "access_token": "[%key:common::config_flow::data::access_token%]",

View File

@ -17,6 +17,13 @@
}, },
"description": "Torna a introduir el token d'acc\u00e9s de desenvolupador d'Awair." "description": "Torna a introduir el token d'acc\u00e9s de desenvolupador d'Awair."
}, },
"reauth_confirm": {
"data": {
"access_token": "Token d'acc\u00e9s",
"email": "Correu electr\u00f2nic"
},
"description": "Torna a introduir el 'token' d'acc\u00e9s de desenvolupador d'Awair."
},
"user": { "user": {
"data": { "data": {
"access_token": "Token d'acc\u00e9s", "access_token": "Token d'acc\u00e9s",

View File

@ -15,7 +15,14 @@
"access_token": "Zugangstoken", "access_token": "Zugangstoken",
"email": "E-Mail" "email": "E-Mail"
}, },
"description": "Bitte gib dein Awair-Entwicklerzugriffstoken erneut ein." "description": "Bitte gib deinen Awair-Entwicklerzugriffstoken erneut ein."
},
"reauth_confirm": {
"data": {
"access_token": "Zugangstoken",
"email": "E-Mail"
},
"description": "Bitte gib deinen Awair-Entwicklerzugriffstoken erneut ein."
}, },
"user": { "user": {
"data": { "data": {

View File

@ -17,6 +17,13 @@
}, },
"description": "Please re-enter your Awair developer access token." "description": "Please re-enter your Awair developer access token."
}, },
"reauth_confirm": {
"data": {
"access_token": "Access Token",
"email": "Email"
},
"description": "Please re-enter your Awair developer access token."
},
"user": { "user": {
"data": { "data": {
"access_token": "Access Token", "access_token": "Access Token",

View File

@ -17,6 +17,13 @@
}, },
"description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba." "description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba."
}, },
"reauth_confirm": {
"data": {
"access_token": "Juurdep\u00e4\u00e4sut\u00f5end",
"email": "Meiliaadress"
},
"description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba."
},
"user": { "user": {
"data": { "data": {
"access_token": "Juurdep\u00e4\u00e4sut\u00f5end", "access_token": "Juurdep\u00e4\u00e4sut\u00f5end",

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