This commit is contained in:
Bram Kragten 2023-09-06 17:16:35 +02:00 committed by GitHub
commit f70469d6ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2706 changed files with 85482 additions and 24628 deletions

View File

@ -30,6 +30,7 @@ base_platforms: &base_platforms
- homeassistant/components/humidifier/** - homeassistant/components/humidifier/**
- homeassistant/components/image/** - homeassistant/components/image/**
- homeassistant/components/image_processing/** - homeassistant/components/image_processing/**
- homeassistant/components/lawn_mower/**
- homeassistant/components/light/** - homeassistant/components/light/**
- homeassistant/components/lock/** - homeassistant/components/lock/**
- homeassistant/components/media_player/** - homeassistant/components/media_player/**
@ -54,6 +55,7 @@ base_platforms: &base_platforms
components: &components components: &components
- homeassistant/components/alexa/** - homeassistant/components/alexa/**
- homeassistant/components/application_credentials/** - homeassistant/components/application_credentials/**
- homeassistant/components/assist_pipeline/**
- homeassistant/components/auth/** - homeassistant/components/auth/**
- homeassistant/components/automation/** - homeassistant/components/automation/**
- homeassistant/components/backup/** - homeassistant/components/backup/**
@ -86,6 +88,7 @@ components: &components
- homeassistant/components/lovelace/** - homeassistant/components/lovelace/**
- homeassistant/components/media_source/** - homeassistant/components/media_source/**
- homeassistant/components/mjpeg/** - homeassistant/components/mjpeg/**
- homeassistant/components/modbus/**
- homeassistant/components/mqtt/** - homeassistant/components/mqtt/**
- homeassistant/components/network/** - homeassistant/components/network/**
- homeassistant/components/onboarding/** - homeassistant/components/onboarding/**

View File

@ -57,6 +57,7 @@ omit =
homeassistant/components/ambiclimate/climate.py homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/__init__.py homeassistant/components/ambient_station/__init__.py
homeassistant/components/ambient_station/binary_sensor.py homeassistant/components/ambient_station/binary_sensor.py
homeassistant/components/ambient_station/entity.py
homeassistant/components/ambient_station/sensor.py homeassistant/components/ambient_station/sensor.py
homeassistant/components/amcrest/* homeassistant/components/amcrest/*
homeassistant/components/ampio/* homeassistant/components/ampio/*
@ -168,6 +169,10 @@ omit =
homeassistant/components/cmus/media_player.py homeassistant/components/cmus/media_player.py
homeassistant/components/coinbase/sensor.py homeassistant/components/coinbase/sensor.py
homeassistant/components/comed_hourly_pricing/sensor.py homeassistant/components/comed_hourly_pricing/sensor.py
homeassistant/components/comelit/__init__.py
homeassistant/components/comelit/const.py
homeassistant/components/comelit/coordinator.py
homeassistant/components/comelit/light.py
homeassistant/components/comfoconnect/fan.py homeassistant/components/comfoconnect/fan.py
homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/alarm_control_panel.py
homeassistant/components/concord232/binary_sensor.py homeassistant/components/concord232/binary_sensor.py
@ -212,10 +217,12 @@ omit =
homeassistant/components/dominos/* homeassistant/components/dominos/*
homeassistant/components/doods/* homeassistant/components/doods/*
homeassistant/components/doorbird/__init__.py homeassistant/components/doorbird/__init__.py
homeassistant/components/doorbird/button.py
homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/camera.py
homeassistant/components/doorbird/button.py
homeassistant/components/doorbird/device.py
homeassistant/components/doorbird/entity.py homeassistant/components/doorbird/entity.py
homeassistant/components/doorbird/util.py homeassistant/components/doorbird/util.py
homeassistant/components/doorbird/view.py
homeassistant/components/dormakaba_dkey/__init__.py homeassistant/components/dormakaba_dkey/__init__.py
homeassistant/components/dormakaba_dkey/binary_sensor.py homeassistant/components/dormakaba_dkey/binary_sensor.py
homeassistant/components/dormakaba_dkey/entity.py homeassistant/components/dormakaba_dkey/entity.py
@ -234,6 +241,7 @@ omit =
homeassistant/components/duotecno/entity.py homeassistant/components/duotecno/entity.py
homeassistant/components/duotecno/switch.py homeassistant/components/duotecno/switch.py
homeassistant/components/duotecno/cover.py homeassistant/components/duotecno/cover.py
homeassistant/components/duotecno/light.py
homeassistant/components/dwd_weather_warnings/const.py homeassistant/components/dwd_weather_warnings/const.py
homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/coordinator.py
homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dwd_weather_warnings/sensor.py
@ -270,6 +278,7 @@ omit =
homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/oauth2.py
homeassistant/components/electric_kiwi/sensor.py homeassistant/components/electric_kiwi/sensor.py
homeassistant/components/electric_kiwi/coordinator.py homeassistant/components/electric_kiwi/coordinator.py
homeassistant/components/electric_kiwi/select.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
@ -282,7 +291,8 @@ omit =
homeassistant/components/elmax/alarm_control_panel.py homeassistant/components/elmax/alarm_control_panel.py
homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/binary_sensor.py
homeassistant/components/elmax/common.py homeassistant/components/elmax/common.py
homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/const.py
homeassistant/components/elmax/cover.py
homeassistant/components/elmax/switch.py homeassistant/components/elmax/switch.py
homeassistant/components/elv/* homeassistant/components/elv/*
homeassistant/components/emby/media_player.py homeassistant/components/emby/media_player.py
@ -299,7 +309,13 @@ omit =
homeassistant/components/enocean/sensor.py homeassistant/components/enocean/sensor.py
homeassistant/components/enocean/switch.py homeassistant/components/enocean/switch.py
homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/__init__.py
homeassistant/components/enphase_envoy/binary_sensor.py
homeassistant/components/enphase_envoy/coordinator.py
homeassistant/components/enphase_envoy/entity.py
homeassistant/components/enphase_envoy/number.py
homeassistant/components/enphase_envoy/select.py
homeassistant/components/enphase_envoy/sensor.py homeassistant/components/enphase_envoy/sensor.py
homeassistant/components/enphase_envoy/switch.py
homeassistant/components/entur_public_transport/* homeassistant/components/entur_public_transport/*
homeassistant/components/environment_canada/__init__.py homeassistant/components/environment_canada/__init__.py
homeassistant/components/environment_canada/camera.py homeassistant/components/environment_canada/camera.py
@ -334,6 +350,7 @@ omit =
homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/entity.py
homeassistant/components/ezviz/select.py homeassistant/components/ezviz/select.py
homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/sensor.py
homeassistant/components/ezviz/siren.py
homeassistant/components/ezviz/switch.py homeassistant/components/ezviz/switch.py
homeassistant/components/ezviz/update.py homeassistant/components/ezviz/update.py
homeassistant/components/faa_delays/__init__.py homeassistant/components/faa_delays/__init__.py
@ -415,6 +432,7 @@ omit =
homeassistant/components/garadget/cover.py homeassistant/components/garadget/cover.py
homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/__init__.py
homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/binary_sensor.py
homeassistant/components/garages_amsterdam/entity.py
homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/garages_amsterdam/sensor.py
homeassistant/components/gc100/* homeassistant/components/gc100/*
homeassistant/components/geniushub/* homeassistant/components/geniushub/*
@ -708,8 +726,6 @@ omit =
homeassistant/components/meteoclimatic/__init__.py homeassistant/components/meteoclimatic/__init__.py
homeassistant/components/meteoclimatic/sensor.py homeassistant/components/meteoclimatic/sensor.py
homeassistant/components/meteoclimatic/weather.py homeassistant/components/meteoclimatic/weather.py
homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py
homeassistant/components/microsoft/tts.py homeassistant/components/microsoft/tts.py
homeassistant/components/mikrotik/hub.py homeassistant/components/mikrotik/hub.py
homeassistant/components/mill/climate.py homeassistant/components/mill/climate.py
@ -764,10 +780,12 @@ omit =
homeassistant/components/neato/__init__.py homeassistant/components/neato/__init__.py
homeassistant/components/neato/api.py homeassistant/components/neato/api.py
homeassistant/components/neato/camera.py homeassistant/components/neato/camera.py
homeassistant/components/neato/entity.py
homeassistant/components/neato/hub.py homeassistant/components/neato/hub.py
homeassistant/components/neato/sensor.py homeassistant/components/neato/sensor.py
homeassistant/components/neato/switch.py homeassistant/components/neato/switch.py
homeassistant/components/neato/vacuum.py homeassistant/components/neato/vacuum.py
homeassistant/components/neato/button.py
homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/netdata/sensor.py homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/__init__.py homeassistant/components/netgear/__init__.py
@ -859,7 +877,6 @@ omit =
homeassistant/components/openhome/const.py homeassistant/components/openhome/const.py
homeassistant/components/openhome/media_player.py homeassistant/components/openhome/media_player.py
homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensensemap/air_quality.py
homeassistant/components/opensky/sensor.py
homeassistant/components/opentherm_gw/__init__.py homeassistant/components/opentherm_gw/__init__.py
homeassistant/components/opentherm_gw/binary_sensor.py homeassistant/components/opentherm_gw/binary_sensor.py
homeassistant/components/opentherm_gw/climate.py homeassistant/components/opentherm_gw/climate.py
@ -989,6 +1006,7 @@ omit =
homeassistant/components/renson/const.py homeassistant/components/renson/const.py
homeassistant/components/renson/entity.py homeassistant/components/renson/entity.py
homeassistant/components/renson/sensor.py homeassistant/components/renson/sensor.py
homeassistant/components/renson/binary_sensor.py
homeassistant/components/raspyrfm/* homeassistant/components/raspyrfm/*
homeassistant/components/recollect_waste/sensor.py homeassistant/components/recollect_waste/sensor.py
homeassistant/components/recorder/repack.py homeassistant/components/recorder/repack.py
@ -1167,7 +1185,13 @@ omit =
homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/browse_media.py
homeassistant/components/squeezebox/media_player.py homeassistant/components/squeezebox/media_player.py
homeassistant/components/starlink/__init__.py
homeassistant/components/starlink/binary_sensor.py
homeassistant/components/starlink/button.py
homeassistant/components/starlink/coordinator.py homeassistant/components/starlink/coordinator.py
homeassistant/components/starlink/device_tracker.py
homeassistant/components/starlink/sensor.py
homeassistant/components/starlink/switch.py
homeassistant/components/starline/__init__.py homeassistant/components/starline/__init__.py
homeassistant/components/starline/account.py homeassistant/components/starline/account.py
homeassistant/components/starline/binary_sensor.py homeassistant/components/starline/binary_sensor.py
@ -1311,9 +1335,6 @@ omit =
homeassistant/components/tplink_omada/__init__.py homeassistant/components/tplink_omada/__init__.py
homeassistant/components/tplink_omada/binary_sensor.py homeassistant/components/tplink_omada/binary_sensor.py
homeassistant/components/tplink_omada/controller.py homeassistant/components/tplink_omada/controller.py
homeassistant/components/tplink_omada/coordinator.py
homeassistant/components/tplink_omada/entity.py
homeassistant/components/tplink_omada/switch.py
homeassistant/components/tplink_omada/update.py homeassistant/components/tplink_omada/update.py
homeassistant/components/traccar/device_tracker.py homeassistant/components/traccar/device_tracker.py
homeassistant/components/tractive/__init__.py homeassistant/components/tractive/__init__.py
@ -1333,9 +1354,11 @@ omit =
homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/__init__.py
homeassistant/components/trafikverket_train/coordinator.py homeassistant/components/trafikverket_train/coordinator.py
homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_train/util.py
homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/__init__.py
homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/coordinator.py
homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py
homeassistant/components/transmission/__init__.py
homeassistant/components/transmission/sensor.py homeassistant/components/transmission/sensor.py
homeassistant/components/transmission/switch.py homeassistant/components/transmission/switch.py
homeassistant/components/travisci/sensor.py homeassistant/components/travisci/sensor.py
@ -1418,6 +1441,10 @@ omit =
homeassistant/components/vlc/media_player.py homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/__init__.py homeassistant/components/vlc_telnet/__init__.py
homeassistant/components/vlc_telnet/media_player.py homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/vodafone_station/__init__.py
homeassistant/components/vodafone_station/const.py
homeassistant/components/vodafone_station/coordinator.py
homeassistant/components/vodafone_station/device_tracker.py
homeassistant/components/volkszaehler/sensor.py homeassistant/components/volkszaehler/sensor.py
homeassistant/components/volumio/__init__.py homeassistant/components/volumio/__init__.py
homeassistant/components/volumio/browse_media.py homeassistant/components/volumio/browse_media.py
@ -1487,7 +1514,6 @@ omit =
homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py
homeassistant/components/yale_smart_alarm/binary_sensor.py homeassistant/components/yale_smart_alarm/binary_sensor.py
homeassistant/components/yale_smart_alarm/button.py homeassistant/components/yale_smart_alarm/button.py
homeassistant/components/yale_smart_alarm/coordinator.py
homeassistant/components/yale_smart_alarm/entity.py homeassistant/components/yale_smart_alarm/entity.py
homeassistant/components/yale_smart_alarm/lock.py homeassistant/components/yale_smart_alarm/lock.py
homeassistant/components/yalexs_ble/__init__.py homeassistant/components/yalexs_ble/__init__.py
@ -1502,6 +1528,9 @@ omit =
homeassistant/components/yamaha_musiccast/select.py homeassistant/components/yamaha_musiccast/select.py
homeassistant/components/yamaha_musiccast/switch.py homeassistant/components/yamaha_musiccast/switch.py
homeassistant/components/yandex_transport/sensor.py homeassistant/components/yandex_transport/sensor.py
homeassistant/components/yardian/__init__.py
homeassistant/components/yardian/coordinator.py
homeassistant/components/yardian/switch.py
homeassistant/components/yeelightsunflower/light.py homeassistant/components/yeelightsunflower/light.py
homeassistant/components/yi/camera.py homeassistant/components/yi/camera.py
homeassistant/components/yolink/__init__.py homeassistant/components/yolink/__init__.py

View File

@ -24,7 +24,7 @@ jobs:
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
with: with:
fetch-depth: 0 fetch-depth: 0
@ -56,7 +56,7 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -98,7 +98,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
@ -197,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.06.1 uses: home-assistant/builder@2023.08.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -251,9 +251,10 @@ jobs:
- raspberrypi4-64 - raspberrypi4-64
- tinker - tinker
- yellow - yellow
- green
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set build additional args - name: Set build additional args
run: | run: |
@ -274,7 +275,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.06.1 uses: home-assistant/builder@2023.08.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -292,7 +293,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@ -330,7 +331,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.1.1 uses: sigstore/cosign-installer@v3.1.1

View File

@ -19,6 +19,10 @@ on:
description: "Skip pytest" description: "Skip pytest"
default: false default: false
type: boolean type: boolean
skip-coverage:
description: "Skip coverage"
default: false
type: boolean
pylint-only: pylint-only:
description: "Only run pylint" description: "Only run pylint"
default: false default: false
@ -32,7 +36,7 @@ env:
CACHE_VERSION: 5 CACHE_VERSION: 5
PIP_CACHE_VERSION: 4 PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 4
HA_SHORT_VERSION: 2023.8 HA_SHORT_VERSION: 2023.9
DEFAULT_PYTHON: "3.11" DEFAULT_PYTHON: "3.11"
ALL_PYTHON_VERSIONS: "['3.11']" ALL_PYTHON_VERSIONS: "['3.11']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@ -79,10 +83,11 @@ jobs:
test_groups: ${{ steps.info.outputs.test_groups }} test_groups: ${{ steps.info.outputs.test_groups }}
tests_glob: ${{ steps.info.outputs.tests_glob }} tests_glob: ${{ steps.info.outputs.tests_glob }}
tests: ${{ steps.info.outputs.tests }} tests: ${{ steps.info.outputs.tests }}
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
id: generate_python_cache_key id: generate_python_cache_key
run: >- run: >-
@ -127,6 +132,7 @@ jobs:
test_group_count=10 test_group_count=10
tests="[]" tests="[]"
tests_glob="" tests_glob=""
skip_coverage=""
if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]]; if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]];
then then
@ -176,6 +182,12 @@ jobs:
test_full_suite="true" test_full_suite="true"
fi fi
if [[ "${{ github.event.inputs.skip-coverage }}" == "true" ]] \
|| [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-skip-coverage') }}" == "true" ]];
then
skip_coverage="true"
fi
# Output & sent to GitHub Actions # Output & sent to GitHub Actions
echo "mariadb_groups: ${mariadb_groups}" echo "mariadb_groups: ${mariadb_groups}"
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
@ -195,6 +207,8 @@ jobs:
echo "tests=${tests}" >> $GITHUB_OUTPUT echo "tests=${tests}" >> $GITHUB_OUTPUT
echo "tests_glob: ${tests_glob}" echo "tests_glob: ${tests_glob}"
echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT
echo "skip_coverage: ${skip_coverage}"
echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT
pre-commit: pre-commit:
name: Prepare pre-commit base name: Prepare pre-commit base
@ -206,7 +220,7 @@ jobs:
- info - info
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -251,7 +265,7 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
id: python id: python
@ -297,7 +311,7 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
id: python id: python
@ -346,7 +360,7 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
id: python id: python
@ -440,7 +454,7 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -508,7 +522,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -540,7 +554,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -573,7 +587,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -617,7 +631,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -699,7 +713,7 @@ jobs:
bluez \ bluez \
ffmpeg ffmpeg
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -734,9 +748,19 @@ jobs:
- name: Run pytest (fully) - name: Run pytest (fully)
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
timeout-minutes: 60 timeout-minutes: 60
id: pytest-full
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant")
cov_params+=(--cov-report=xml)
fi
python3 -X dev -m pytest \ python3 -X dev -m pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
@ -745,37 +769,54 @@ jobs:
--dist=loadfile \ --dist=loadfile \
--test-group-count ${{ needs.info.outputs.test_group_count }} \ --test-group-count ${{ needs.info.outputs.test_group_count }} \
--test-group=${{ matrix.group }} \ --test-group=${{ matrix.group }} \
--cov="homeassistant" \ ${cov_params[@]} \
--cov-report=xml \
-o console_output_style=count \ -o console_output_style=count \
-p no:sugar \ -p no:sugar \
tests tests \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Run pytest (partially) - name: Run pytest (partially)
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
timeout-minutes: 10 timeout-minutes: 10
id: pytest-partial
shell: bash shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then
echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py" echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py"
exit 1 exit 1
fi fi
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.${{ matrix.group }}")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -X dev -m pytest \ python3 -X dev -m pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
-n auto \ -n auto \
--cov="homeassistant.components.${{ matrix.group }}" \ ${cov_params[@]} \
--cov-report=xml \
--cov-report=term-missing \
-o console_output_style=count \ -o console_output_style=count \
--durations=0 \ --durations=0 \
--durations-min=1 \ --durations-min=1 \
-p no:sugar \ -p no:sugar \
tests/components/${{ matrix.group }} tests/components/${{ matrix.group }} \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure')
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v3.1.2
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
@ -824,7 +865,7 @@ jobs:
ffmpeg \ ffmpeg \
libmariadb-dev-compat libmariadb-dev-compat
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -862,18 +903,27 @@ jobs:
python3 -m script.translations develop --all python3 -m script.translations develop --all
- name: Run pytest (partially) - name: Run pytest (partially)
timeout-minutes: 20 timeout-minutes: 20
id: pytest-partial
shell: bash shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
mariadb=$(echo "${{ matrix.mariadb-group }}" | sed "s/:/-/g")
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.recorder")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -X dev -m pytest \ python3 -X dev -m pytest \
-qq \ -qq \
--timeout=20 \ --timeout=20 \
-n 1 \ -n 1 \
--cov="homeassistant.components.recorder" \ ${cov_params[@]} \
--cov-report=xml \
--cov-report=term-missing \
-o console_output_style=count \ -o console_output_style=count \
--durations=10 \ --durations=10 \
-p no:sugar \ -p no:sugar \
@ -881,8 +931,16 @@ jobs:
tests/components/history \ tests/components/history \
tests/components/logbook \ tests/components/logbook \
tests/components/recorder \ tests/components/recorder \
tests/components/sensor tests/components/sensor \
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v3.1.2
with: with:
name: coverage-${{ matrix.python-version }}-mariadb name: coverage-${{ matrix.python-version }}-mariadb
@ -931,7 +989,7 @@ jobs:
ffmpeg \ ffmpeg \
postgresql-server-dev-14 postgresql-server-dev-14
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0
@ -969,18 +1027,27 @@ jobs:
python3 -m script.translations develop --all python3 -m script.translations develop --all
- name: Run pytest (partially) - name: Run pytest (partially)
timeout-minutes: 20 timeout-minutes: 20
id: pytest-partial
shell: bash shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
set -o pipefail
postgresql=$(echo "${{ matrix.postgresql-group }}" | sed "s/:/-/g")
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.recorder")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -X dev -m pytest \ python3 -X dev -m pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
-n 1 \ -n 1 \
--cov="homeassistant.components.recorder" \ ${cov_params[@]} \
--cov-report=xml \
--cov-report=term-missing \
-o console_output_style=count \ -o console_output_style=count \
--durations=0 \ --durations=0 \
--durations-min=10 \ --durations-min=10 \
@ -989,8 +1056,16 @@ jobs:
tests/components/history \ tests/components/history \
tests/components/logbook \ tests/components/logbook \
tests/components/recorder \ tests/components/recorder \
tests/components/sensor tests/components/sensor \
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.0 uses: actions/upload-artifact@v3.1.0
with: with:
name: coverage-${{ matrix.python-version }}-postgresql name: coverage-${{ matrix.python-version }}-postgresql
@ -1001,6 +1076,7 @@ jobs:
coverage: coverage:
name: Upload test coverage to Codecov name: Upload test coverage to Codecov
if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: needs:
- info - info
@ -1008,7 +1084,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
- name: Upload coverage to Codecov (full coverage) - name: Upload coverage to Codecov (full coverage)

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.0

View File

@ -26,7 +26,7 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }} architectures: ${{ steps.info.outputs.architectures }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Get information - name: Get information
id: info id: info
@ -84,7 +84,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
@ -122,7 +122,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.3 uses: actions/checkout@v3.6.0
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3

1
.gitignore vendored
View File

@ -67,6 +67,7 @@ htmlcov/
test-reports/ test-reports/
test-results.xml test-results.xml
test-output.xml test-output.xml
pytest-*.txt
# Translations # Translations
*.mo *.mo

View File

@ -1,11 +1,11 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.280 rev: v0.0.285
hooks: hooks:
- id: ruff - id: ruff
args: args:
- --fix - --fix
- repo: https://github.com/psf/black - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.7.0 rev: 23.7.0
hooks: hooks:
- id: black - id: black
@ -43,7 +43,7 @@ repos:
hooks: hooks:
- id: prettier - id: prettier
- repo: https://github.com/cdce8p/python-typing-update - repo: https://github.com/cdce8p/python-typing-update
rev: v0.5.0 rev: v0.6.0
hooks: hooks:
# Run `python-typing-update` hook manually from time to time # Run `python-typing-update` hook manually from time to time
# to update python typing syntax. # to update python typing syntax.
@ -52,7 +52,7 @@ repos:
- id: python-typing-update - id: python-typing-update
stages: [manual] stages: [manual]
args: args:
- --py310-plus - --py311-plus
- --force - --force
- --keep-updates - --keep-updates
files: ^(homeassistant|tests|script)/.+\.py$ files: ^(homeassistant|tests|script)/.+\.py$

View File

@ -53,6 +53,7 @@ homeassistant.components.airzone_cloud.*
homeassistant.components.aladdin_connect.* homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.* homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.* homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.ambient_station.* homeassistant.components.ambient_station.*
homeassistant.components.amcrest.* homeassistant.components.amcrest.*
@ -103,6 +104,7 @@ homeassistant.components.dhcp.*
homeassistant.components.diagnostics.* homeassistant.components.diagnostics.*
homeassistant.components.dlna_dmr.* homeassistant.components.dlna_dmr.*
homeassistant.components.dnsip.* homeassistant.components.dnsip.*
homeassistant.components.doorbird.*
homeassistant.components.dormakaba_dkey.* homeassistant.components.dormakaba_dkey.*
homeassistant.components.dsmr.* homeassistant.components.dsmr.*
homeassistant.components.dunehd.* homeassistant.components.dunehd.*
@ -147,6 +149,7 @@ homeassistant.components.history.*
homeassistant.components.homeassistant.exposed_entities homeassistant.components.homeassistant.exposed_entities
homeassistant.components.homeassistant.triggers.event homeassistant.components.homeassistant.triggers.event
homeassistant.components.homeassistant_alerts.* homeassistant.components.homeassistant_alerts.*
homeassistant.components.homeassistant_green.*
homeassistant.components.homeassistant_hardware.* homeassistant.components.homeassistant_hardware.*
homeassistant.components.homeassistant_sky_connect.* homeassistant.components.homeassistant_sky_connect.*
homeassistant.components.homeassistant_yellow.* homeassistant.components.homeassistant_yellow.*
@ -181,6 +184,7 @@ homeassistant.components.imap.*
homeassistant.components.input_button.* homeassistant.components.input_button.*
homeassistant.components.input_select.* homeassistant.components.input_select.*
homeassistant.components.integration.* homeassistant.components.integration.*
homeassistant.components.ipp.*
homeassistant.components.iqvia.* homeassistant.components.iqvia.*
homeassistant.components.isy994.* homeassistant.components.isy994.*
homeassistant.components.jellyfin.* homeassistant.components.jellyfin.*
@ -193,6 +197,7 @@ homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.* homeassistant.components.lacrosse_view.*
homeassistant.components.lametric.* homeassistant.components.lametric.*
homeassistant.components.laundrify.* homeassistant.components.laundrify.*
homeassistant.components.lawn_mower.*
homeassistant.components.lcn.* homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.* homeassistant.components.ld2410_ble.*
homeassistant.components.lidarr.* homeassistant.components.lidarr.*
@ -209,6 +214,7 @@ homeassistant.components.luftdaten.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*
homeassistant.components.mastodon.* homeassistant.components.mastodon.*
homeassistant.components.matter.* homeassistant.components.matter.*
homeassistant.components.media_extractor.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.media_source.* homeassistant.components.media_source.*
homeassistant.components.metoffice.* homeassistant.components.metoffice.*
@ -296,6 +302,7 @@ homeassistant.components.sonarr.*
homeassistant.components.speedtestdotnet.* homeassistant.components.speedtestdotnet.*
homeassistant.components.sql.* homeassistant.components.sql.*
homeassistant.components.ssdp.* homeassistant.components.ssdp.*
homeassistant.components.starlink.*
homeassistant.components.statistics.* homeassistant.components.statistics.*
homeassistant.components.steamist.* homeassistant.components.steamist.*
homeassistant.components.stookalert.* homeassistant.components.stookalert.*
@ -321,6 +328,7 @@ homeassistant.components.tplink.*
homeassistant.components.tplink_omada.* homeassistant.components.tplink_omada.*
homeassistant.components.tractive.* homeassistant.components.tractive.*
homeassistant.components.tradfri.* homeassistant.components.tradfri.*
homeassistant.components.trafikverket_camera.*
homeassistant.components.trafikverket_ferry.* homeassistant.components.trafikverket_ferry.*
homeassistant.components.trafikverket_train.* homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.* homeassistant.components.trafikverket_weatherstation.*

View File

@ -19,8 +19,6 @@ build.json @home-assistant/supervisor
# Other code # Other code
/homeassistant/scripts/check_config.py @kellerza /homeassistant/scripts/check_config.py @kellerza
/homeassistant/const.py @epenet
/homeassistant/util/ @epenet
# Integrations # Integrations
/homeassistant/components/abode/ @shred86 /homeassistant/components/abode/ @shred86
@ -51,8 +49,6 @@ build.json @home-assistant/supervisor
/tests/components/airthings/ @danielhiversen /tests/components/airthings/ @danielhiversen
/homeassistant/components/airthings_ble/ @vincegio /homeassistant/components/airthings_ble/ @vincegio
/tests/components/airthings_ble/ @vincegio /tests/components/airthings_ble/ @vincegio
/homeassistant/components/airtouch4/ @LonePurpleWolf
/tests/components/airtouch4/ @LonePurpleWolf
/homeassistant/components/airvisual/ @bachya /homeassistant/components/airvisual/ @bachya
/tests/components/airvisual/ @bachya /tests/components/airvisual/ @bachya
/homeassistant/components/airvisual_pro/ @bachya /homeassistant/components/airvisual_pro/ @bachya
@ -211,6 +207,8 @@ build.json @home-assistant/supervisor
/tests/components/coinbase/ @tombrien /tests/components/coinbase/ @tombrien
/homeassistant/components/color_extractor/ @GenericStudent /homeassistant/components/color_extractor/ @GenericStudent
/tests/components/color_extractor/ @GenericStudent /tests/components/color_extractor/ @GenericStudent
/homeassistant/components/comelit/ @chemelli74
/tests/components/comelit/ @chemelli74
/homeassistant/components/comfoconnect/ @michaelarnauts /homeassistant/components/comfoconnect/ @michaelarnauts
/tests/components/comfoconnect/ @michaelarnauts /tests/components/comfoconnect/ @michaelarnauts
/homeassistant/components/command_line/ @gjohansson-ST /homeassistant/components/command_line/ @gjohansson-ST
@ -295,12 +293,10 @@ build.json @home-assistant/supervisor
/tests/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @depl0y @glodenox /homeassistant/components/dsmr_reader/ @depl0y @glodenox
/tests/components/dsmr_reader/ @depl0y @glodenox /tests/components/dsmr_reader/ @depl0y @glodenox
/homeassistant/components/dunehd/ @bieniu
/tests/components/dunehd/ @bieniu
/homeassistant/components/duotecno/ @cereal2nd /homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo /tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/homeassistant/components/dynalite/ @ziv1234 /homeassistant/components/dynalite/ @ziv1234
/tests/components/dynalite/ @ziv1234 /tests/components/dynalite/ @ziv1234
/homeassistant/components/eafm/ @Jc2k /homeassistant/components/eafm/ @Jc2k
@ -345,8 +341,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/enigma2/ @fbradyirl /homeassistant/components/enigma2/ @fbradyirl
/homeassistant/components/enocean/ @bdurrer /homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @gtdiehl /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
/tests/components/enphase_envoy/ @gtdiehl /tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
/homeassistant/components/entur_public_transport/ @hfurubotten /homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie
@ -525,6 +521,8 @@ build.json @home-assistant/supervisor
/tests/components/homeassistant/ @home-assistant/core /tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_alerts/ @home-assistant/core /homeassistant/components/homeassistant_alerts/ @home-assistant/core
/tests/components/homeassistant_alerts/ @home-assistant/core /tests/components/homeassistant_alerts/ @home-assistant/core
/homeassistant/components/homeassistant_green/ @home-assistant/core
/tests/components/homeassistant_green/ @home-assistant/core
/homeassistant/components/homeassistant_hardware/ @home-assistant/core /homeassistant/components/homeassistant_hardware/ @home-assistant/core
/tests/components/homeassistant_hardware/ @home-assistant/core /tests/components/homeassistant_hardware/ @home-assistant/core
/homeassistant/components/homeassistant_sky_connect/ @home-assistant/core /homeassistant/components/homeassistant_sky_connect/ @home-assistant/core
@ -608,8 +606,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard /homeassistant/components/iotawatt/ @gtdiehl @jyavenard
/tests/components/iotawatt/ @gtdiehl @jyavenard /tests/components/iotawatt/ @gtdiehl @jyavenard
/homeassistant/components/iperf3/ @rohankapoorcom /homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes @abmantis /homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes @abmantis /tests/components/ipma/ @dgomes
/homeassistant/components/ipp/ @ctalkington /homeassistant/components/ipp/ @ctalkington
/tests/components/ipp/ @ctalkington /tests/components/ipp/ @ctalkington
/homeassistant/components/iqvia/ @bachya /homeassistant/components/iqvia/ @bachya
@ -673,6 +671,8 @@ build.json @home-assistant/supervisor
/tests/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol
/homeassistant/components/laundrify/ @xLarry /homeassistant/components/laundrify/ @xLarry
/tests/components/laundrify/ @xLarry /tests/components/laundrify/ @xLarry
/homeassistant/components/lawn_mower/ @home-assistant/core
/tests/components/lawn_mower/ @home-assistant/core
/homeassistant/components/lcn/ @alengwenus /homeassistant/components/lcn/ @alengwenus
/tests/components/lcn/ @alengwenus /tests/components/lcn/ @alengwenus
/homeassistant/components/ld2410_ble/ @930913 /homeassistant/components/ld2410_ble/ @930913
@ -729,6 +729,7 @@ build.json @home-assistant/supervisor
/tests/components/mazda/ @bdr99 /tests/components/mazda/ @bdr99
/homeassistant/components/meater/ @Sotolotl @emontnemery /homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery /tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/media_extractor/ @joostlek
/homeassistant/components/media_player/ @home-assistant/core /homeassistant/components/media_player/ @home-assistant/core
/tests/components/media_player/ @home-assistant/core /tests/components/media_player/ @home-assistant/core
/homeassistant/components/media_source/ @hunterjm /homeassistant/components/media_source/ @hunterjm
@ -1028,8 +1029,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/repairs/ @home-assistant/core /homeassistant/components/repairs/ @home-assistant/core
/tests/components/repairs/ @home-assistant/core /tests/components/repairs/ @home-assistant/core
/homeassistant/components/repetier/ @MTrab @ShadowBr0ther /homeassistant/components/repetier/ @MTrab @ShadowBr0ther
/homeassistant/components/rest/ @epenet
/tests/components/rest/ @epenet
/homeassistant/components/rflink/ @javicalle /homeassistant/components/rflink/ @javicalle
/tests/components/rflink/ @javicalle /tests/components/rflink/ @javicalle
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
@ -1058,8 +1057,8 @@ build.json @home-assistant/supervisor
/tests/components/rss_feed_template/ @home-assistant/core /tests/components/rss_feed_template/ @home-assistant/core
/homeassistant/components/rtsp_to_webrtc/ @allenporter /homeassistant/components/rtsp_to_webrtc/ @allenporter
/tests/components/rtsp_to_webrtc/ @allenporter /tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @gabe565 /homeassistant/components/ruckus_unleashed/ @gabe565 @lanrat
/tests/components/ruckus_unleashed/ @gabe565 /tests/components/ruckus_unleashed/ @gabe565 @lanrat
/homeassistant/components/ruuvi_gateway/ @akx /homeassistant/components/ruuvi_gateway/ @akx
/tests/components/ruuvi_gateway/ @akx /tests/components/ruuvi_gateway/ @akx
/homeassistant/components/ruuvitag_ble/ @akx /homeassistant/components/ruuvitag_ble/ @akx
@ -1077,9 +1076,11 @@ build.json @home-assistant/supervisor
/tests/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core
/homeassistant/components/schedule/ @home-assistant/core /homeassistant/components/schedule/ @home-assistant/core
/tests/components/schedule/ @home-assistant/core /tests/components/schedule/ @home-assistant/core
/homeassistant/components/schlage/ @dknowles2
/tests/components/schlage/ @dknowles2
/homeassistant/components/schluter/ @prairieapps /homeassistant/components/schluter/ @prairieapps
/homeassistant/components/scrape/ @fabaff @gjohansson-ST @epenet /homeassistant/components/scrape/ @fabaff @gjohansson-ST
/tests/components/scrape/ @fabaff @gjohansson-ST @epenet /tests/components/scrape/ @fabaff @gjohansson-ST
/homeassistant/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/screenlogic/ @dieselrabbit @bdraco
/tests/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco
/homeassistant/components/script/ @home-assistant/core /homeassistant/components/script/ @home-assistant/core
@ -1226,8 +1227,8 @@ build.json @home-assistant/supervisor
/tests/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core
/homeassistant/components/switchbee/ @jafar-atili /homeassistant/components/switchbee/ @jafar-atili
/tests/components/switchbee/ @jafar-atili /tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switcher_kis/ @thecode /homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode /tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/switchmate/ @danielhiversen @qiz-li
@ -1298,6 +1299,8 @@ build.json @home-assistant/supervisor
/tests/components/trace/ @home-assistant/core /tests/components/trace/ @home-assistant/core
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu /homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
/tests/components/tractive/ @Danielhiversen @zhulik @bieniu /tests/components/tractive/ @Danielhiversen @zhulik @bieniu
/homeassistant/components/trafikverket_camera/ @gjohansson-ST
/tests/components/trafikverket_camera/ @gjohansson-ST
/homeassistant/components/trafikverket_ferry/ @gjohansson-ST /homeassistant/components/trafikverket_ferry/ @gjohansson-ST
/tests/components/trafikverket_ferry/ @gjohansson-ST /tests/components/trafikverket_ferry/ @gjohansson-ST
/homeassistant/components/trafikverket_train/ @endor-force @gjohansson-ST /homeassistant/components/trafikverket_train/ @endor-force @gjohansson-ST
@ -1365,6 +1368,8 @@ build.json @home-assistant/supervisor
/tests/components/vizio/ @raman325 /tests/components/vizio/ @raman325
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare /homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare /tests/components/vlc_telnet/ @rodripf @MartinHjelmare
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
/tests/components/vodafone_station/ @paoloantinori @chemelli74
/homeassistant/components/voip/ @balloob @synesthesiam /homeassistant/components/voip/ @balloob @synesthesiam
/tests/components/voip/ @balloob @synesthesiam /tests/components/voip/ @balloob @synesthesiam
/homeassistant/components/volumio/ @OnFreund /homeassistant/components/volumio/ @OnFreund
@ -1375,6 +1380,8 @@ build.json @home-assistant/supervisor
/tests/components/vulcan/ @Antoni-Czaplicki /tests/components/vulcan/ @Antoni-Czaplicki
/homeassistant/components/wake_on_lan/ @ntilley905 /homeassistant/components/wake_on_lan/ @ntilley905
/tests/components/wake_on_lan/ @ntilley905 /tests/components/wake_on_lan/ @ntilley905
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
/tests/components/wake_word/ @home-assistant/core @synesthesiam
/homeassistant/components/wallbox/ @hesselonline /homeassistant/components/wallbox/ @hesselonline
/tests/components/wallbox/ @hesselonline /tests/components/wallbox/ @hesselonline
/homeassistant/components/waqi/ @andrey-git /homeassistant/components/waqi/ @andrey-git
@ -1438,6 +1445,7 @@ build.json @home-assistant/supervisor
/tests/components/yamaha_musiccast/ @vigonotion @micha91 /tests/components/yamaha_musiccast/ @vigonotion @micha91
/homeassistant/components/yandex_transport/ @rishatik92 @devbis /homeassistant/components/yandex_transport/ @rishatik92 @devbis
/tests/components/yandex_transport/ @rishatik92 @devbis /tests/components/yandex_transport/ @rishatik92 @devbis
/homeassistant/components/yardian/ @h3l1o5
/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 /homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 /tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
/homeassistant/components/yeelightsunflower/ @lindsaymarkward /homeassistant/components/yeelightsunflower/ @lindsaymarkward

View File

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

View File

@ -148,16 +148,6 @@ def get_arguments() -> argparse.Namespace:
return arguments return arguments
def cmdline() -> list[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if os.path.basename(sys.argv[0]) == "__main__.py":
modulepath = os.path.dirname(sys.argv[0])
os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
return [sys.executable, "-m", "homeassistant"] + list(sys.argv[1:])
return sys.argv
def check_threads() -> None: def check_threads() -> None:
"""Check if there are any lingering threads.""" """Check if there are any lingering threads."""
try: try:

View File

@ -8,7 +8,7 @@ from typing import Any, Generic, Self, TypeVar, overload
_T = TypeVar("_T") _T = TypeVar("_T")
class cached_property(Generic[_T]): # pylint: disable=invalid-name class cached_property(Generic[_T]):
"""Backport of Python 3.12's cached_property. """Backport of Python 3.12's cached_property.
Includes https://github.com/python/cpython/pull/101890/files Includes https://github.com/python/cpython/pull/101890/files

View File

@ -110,8 +110,7 @@ async def async_setup_hass(
runtime_config: RuntimeConfig, runtime_config: RuntimeConfig,
) -> core.HomeAssistant | None: ) -> core.HomeAssistant | None:
"""Set up Home Assistant.""" """Set up Home Assistant."""
hass = core.HomeAssistant() hass = core.HomeAssistant(runtime_config.config_dir)
hass.config.config_dir = runtime_config.config_dir
async_enable_logging( async_enable_logging(
hass, hass,
@ -134,6 +133,7 @@ async def async_setup_hass(
_LOGGER.info("Config directory: %s", runtime_config.config_dir) _LOGGER.info("Config directory: %s", runtime_config.config_dir)
loader.async_setup(hass)
config_dict = None config_dict = None
basic_setup_success = False basic_setup_success = False
@ -177,14 +177,15 @@ async def async_setup_hass(
old_config = hass.config old_config = hass.config
old_logging = hass.data.get(DATA_LOGGING) old_logging = hass.data.get(DATA_LOGGING)
hass = core.HomeAssistant() hass = core.HomeAssistant(old_config.config_dir)
if old_logging: if old_logging:
hass.data[DATA_LOGGING] = old_logging hass.data[DATA_LOGGING] = old_logging
hass.config.skip_pip = old_config.skip_pip hass.config.skip_pip = old_config.skip_pip
hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.skip_pip_packages = old_config.skip_pip_packages
hass.config.internal_url = old_config.internal_url hass.config.internal_url = old_config.internal_url
hass.config.external_url = old_config.external_url hass.config.external_url = old_config.external_url
hass.config.config_dir = old_config.config_dir # Setup loader cache after the config dir has been set
loader.async_setup(hass)
if safe_mode: if safe_mode:
_LOGGER.info("Starting in safe mode") _LOGGER.info("Starting in safe mode")

View File

@ -2,6 +2,7 @@
"domain": "trafikverket", "domain": "trafikverket",
"name": "Trafikverket", "name": "Trafikverket",
"integrations": [ "integrations": [
"trafikverket_camera",
"trafikverket_ferry", "trafikverket_ferry",
"trafikverket_train", "trafikverket_train",
"trafikverket_weatherstation" "trafikverket_weatherstation"

View File

@ -28,6 +28,7 @@ from homeassistant.const import (
from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, entity from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
@ -287,14 +288,14 @@ class AbodeDevice(AbodeEntity):
"""Initialize Abode device.""" """Initialize Abode device."""
super().__init__(data) super().__init__(data)
self._device = device self._device = device
self._attr_unique_id = device.device_uuid self._attr_unique_id = device.uuid
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to device events.""" """Subscribe to device events."""
await super().async_added_to_hass() await super().async_added_to_hass()
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
self._data.abode.events.add_device_callback, self._data.abode.events.add_device_callback,
self._device.device_id, self._device.id,
self._update_callback, self._update_callback,
) )
@ -302,7 +303,7 @@ class AbodeDevice(AbodeEntity):
"""Unsubscribe from device events.""" """Unsubscribe from device events."""
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
self._data.abode.events.remove_all_device_callbacks, self._device.device_id self._data.abode.events.remove_all_device_callbacks, self._device.id
) )
def update(self) -> None: def update(self) -> None:
@ -313,17 +314,17 @@ class AbodeDevice(AbodeEntity):
def extra_state_attributes(self) -> dict[str, str]: def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes.""" """Return the state attributes."""
return { return {
"device_id": self._device.device_id, "device_id": self._device.id,
"battery_low": self._device.battery_low, "battery_low": self._device.battery_low,
"no_response": self._device.no_response, "no_response": self._device.no_response,
"device_type": self._device.type, "device_type": self._device.type,
} }
@property @property
def device_info(self) -> entity.DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity.""" """Return device registry information for this entity."""
return entity.DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self._device.device_id)}, identifiers={(DOMAIN, self._device.id)},
manufacturer="Abode", manufacturer="Abode",
model=self._device.type, model=self._device.type,
name=self._device.name, name=self._device.name,

View File

@ -69,7 +69,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
def extra_state_attributes(self) -> dict[str, str]: def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes.""" """Return the state attributes."""
return { return {
"device_id": self._device.device_id, "device_id": self._device.id,
"battery_backup": self._device.battery, "battery_backup": self._device.battery,
"cellular_backup": self._device.is_cellular, "cellular_backup": self._device.is_cellular,
} }

View File

@ -30,7 +30,7 @@ async def async_setup_entry(
data: AbodeSystem = hass.data[DOMAIN] data: AbodeSystem = hass.data[DOMAIN]
async_add_entities( async_add_entities(
AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) # pylint: disable=no-member AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA)
) )

View File

@ -1,6 +1,8 @@
"""Support for Abode Security System sensors.""" """Support for Abode Security System sensors."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import cast from typing import cast
from jaraco.abode.devices.sensor import Sensor as AbodeSense from jaraco.abode.devices.sensor import Sensor as AbodeSense
@ -12,25 +14,52 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
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 . import AbodeDevice, AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ABODE_TEMPERATURE_UNIT_HA_UNIT = {
SensorEntityDescription( CONST.UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
CONST.UNIT_CELSIUS: UnitOfTemperature.CELSIUS,
}
@dataclass
class AbodeSensorDescriptionMixin:
"""Mixin for Abode sensor."""
value_fn: Callable[[AbodeSense], float]
native_unit_of_measurement_fn: Callable[[AbodeSense], str]
@dataclass
class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin):
"""Class describing Abode sensor entities."""
SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
AbodeSensorDescription(
key=CONST.TEMP_STATUS_KEY, key=CONST.TEMP_STATUS_KEY,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
device.temp_unit
],
value_fn=lambda device: cast(float, device.temp),
), ),
SensorEntityDescription( AbodeSensorDescription(
key=CONST.HUMI_STATUS_KEY, key=CONST.HUMI_STATUS_KEY,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
value_fn=lambda device: cast(float, device.humidity),
), ),
SensorEntityDescription( AbodeSensorDescription(
key=CONST.LUX_STATUS_KEY, key=CONST.LUX_STATUS_KEY,
device_class=SensorDeviceClass.ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
value_fn=lambda device: cast(float, device.lux),
), ),
) )
@ -52,32 +81,26 @@ async def async_setup_entry(
class AbodeSensor(AbodeDevice, SensorEntity): class AbodeSensor(AbodeDevice, SensorEntity):
"""A sensor implementation for Abode devices.""" """A sensor implementation for Abode devices."""
entity_description: AbodeSensorDescription
_device: AbodeSense _device: AbodeSense
def __init__( def __init__(
self, self,
data: AbodeSystem, data: AbodeSystem,
device: AbodeSense, device: AbodeSense,
description: SensorEntityDescription, description: AbodeSensorDescription,
) -> None: ) -> None:
"""Initialize a sensor for an Abode device.""" """Initialize a sensor for an Abode device."""
super().__init__(data, device) super().__init__(data, device)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{device.device_uuid}-{description.key}" self._attr_unique_id = f"{device.uuid}-{description.key}"
if description.key == CONST.TEMP_STATUS_KEY:
self._attr_native_unit_of_measurement = device.temp_unit
elif description.key == CONST.HUMI_STATUS_KEY:
self._attr_native_unit_of_measurement = device.humidity_unit
elif description.key == CONST.LUX_STATUS_KEY:
self._attr_native_unit_of_measurement = LIGHT_LUX
@property @property
def native_value(self) -> float | None: def native_value(self) -> float:
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self.entity_description.key == CONST.TEMP_STATUS_KEY: return self.entity_description.value_fn(self._device)
return cast(float, self._device.temp)
if self.entity_description.key == CONST.HUMI_STATUS_KEY: @property
return cast(float, self._device.humidity) def native_unit_of_measurement(self) -> str:
if self.entity_description.key == CONST.LUX_STATUS_KEY: """Return the native unit of measurement."""
return cast(float, self._device.lux) return self.entity_description.native_unit_of_measurement_fn(self._device)
return None

View File

@ -1,6 +1,7 @@
"""The AccuWeather component.""" """The AccuWeather component."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
@ -8,7 +9,6 @@ from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientSession from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -16,8 +16,7 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER

View File

@ -2,12 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from typing import Any from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries

View File

@ -50,3 +50,8 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = {
ATTR_CONDITION_SUNNY: [1, 2, 5], ATTR_CONDITION_SUNNY: [1, 2, 5],
ATTR_CONDITION_WINDY: [32], ATTR_CONDITION_WINDY: [32],
} }
CONDITION_MAP = {
cond_code: cond_ha
for cond_ha, cond_codes in CONDITION_CLASSES.items()
for cond_code in cond_codes
}

View File

@ -59,193 +59,280 @@ class AccuWeatherSensorDescription(
"""Class describing AccuWeather sensor entities.""" """Class describing AccuWeather sensor entities."""
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
day: int | None = None
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="AirQuality", key="AirQuality",
icon="mdi:air-filter", icon="mdi:air-filter",
name="Air quality",
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"], options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
translation_key="air_quality", translation_key=f"air_quality_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="CloudCoverDay", key="CloudCoverDay",
icon="mdi:weather-cloudy", icon="mdi:weather-cloudy",
name="Cloud cover day",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
translation_key=f"cloud_cover_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="CloudCoverNight", key="CloudCoverNight",
icon="mdi:weather-cloudy", icon="mdi:weather-cloudy",
name="Cloud cover night",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
translation_key=f"cloud_cover_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Grass", key="Grass",
icon="mdi:grass", icon="mdi:grass",
name="Grass pollen",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]), value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="grass_pollen", translation_key=f"grass_pollen_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="HoursOfSun", key="HoursOfSun",
icon="mdi:weather-partly-cloudy", icon="mdi:weather-partly-cloudy",
name="Hours of sun",
native_unit_of_measurement=UnitOfTime.HOURS, native_unit_of_measurement=UnitOfTime.HOURS,
value_fn=lambda data: cast(float, data), value_fn=lambda data: cast(float, data),
translation_key=f"hours_of_sun_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="LongPhraseDay", key="LongPhraseDay",
name="Condition day",
value_fn=lambda data: cast(str, data), value_fn=lambda data: cast(str, data),
translation_key=f"condition_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="LongPhraseNight", key="LongPhraseNight",
name="Condition night",
value_fn=lambda data: cast(str, data), value_fn=lambda data: cast(str, data),
translation_key=f"condition_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Mold", key="Mold",
icon="mdi:blur", icon="mdi:blur",
name="Mold pollen",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]), value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="mold_pollen", translation_key=f"mold_pollen_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Ragweed", key="Ragweed",
icon="mdi:sprout", icon="mdi:sprout",
name="Ragweed pollen",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]), value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="ragweed_pollen", translation_key=f"ragweed_pollen_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureMax", key="RealFeelTemperatureMax",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature max",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_max_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureMin", key="RealFeelTemperatureMin",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature min",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_min_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMax", key="RealFeelTemperatureShadeMax",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature shade max",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_shade_max_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMin", key="RealFeelTemperatureShadeMin",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature shade min",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_shade_min_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="SolarIrradianceDay", key="SolarIrradianceDay",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
name="Solar irradiance day",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
value_fn=lambda data: cast(float, data[ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"solar_irradiance_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="SolarIrradianceNight", key="SolarIrradianceNight",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
name="Solar irradiance night",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
value_fn=lambda data: cast(float, data[ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"solar_irradiance_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="ThunderstormProbabilityDay", key="ThunderstormProbabilityDay",
icon="mdi:weather-lightning", icon="mdi:weather-lightning",
name="Thunderstorm probability day",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
translation_key=f"thunderstorm_probability_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="ThunderstormProbabilityNight", key="ThunderstormProbabilityNight",
icon="mdi:weather-lightning", icon="mdi:weather-lightning",
name="Thunderstorm probability night",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
translation_key=f"thunderstorm_probability_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Tree", key="Tree",
icon="mdi:tree-outline", icon="mdi:tree-outline",
name="Tree pollen",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]), value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="tree_pollen", translation_key=f"tree_pollen_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="UVIndex", key="UVIndex",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
name="UV index",
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
value_fn=lambda data: cast(int, data[ATTR_VALUE]), value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="uv_index", translation_key=f"uv_index_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="WindGustDay", key="WindGustDay",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
name="Wind gust day",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_gust_speed_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="WindGustNight", key="WindGustNight",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
name="Wind gust night",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_gust_speed_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="WindDay", key="WindDay",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
name="Wind day",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_speed_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
*(
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="WindNight", key="WindNight",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
name="Wind night",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_speed_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
), ),
) )
@ -253,118 +340,117 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="ApparentTemperature", key="ApparentTemperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="Apparent temperature",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="apparent_temperature",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Ceiling", key="Ceiling",
device_class=SensorDeviceClass.DISTANCE, device_class=SensorDeviceClass.DISTANCE,
icon="mdi:weather-fog", icon="mdi:weather-fog",
name="Cloud ceiling",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfLength.METERS, native_unit_of_measurement=UnitOfLength.METERS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
suggested_display_precision=0, suggested_display_precision=0,
translation_key="cloud_ceiling",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="CloudCover", key="CloudCover",
icon="mdi:weather-cloudy", icon="mdi:weather-cloudy",
name="Cloud cover",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
translation_key="cloud_cover",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="DewPoint", key="DewPoint",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="Dew point",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="dew_point",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperature", key="RealFeelTemperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="realfeel_temperature",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureShade", key="RealFeelTemperatureShade",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature shade",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="realfeel_temperature_shade",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Precipitation", key="Precipitation",
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
name="Precipitation",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
attr_fn=lambda data: {"type": data["PrecipitationType"]}, attr_fn=lambda data: {"type": data["PrecipitationType"]},
translation_key="precipitation",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="PressureTendency", key="PressureTendency",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
icon="mdi:gauge", icon="mdi:gauge",
name="Pressure tendency",
options=["falling", "rising", "steady"], options=["falling", "rising", "steady"],
translation_key="pressure_tendency",
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(), value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
translation_key="pressure_tendency",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="UVIndex", key="UVIndex",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
name="UV index",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
value_fn=lambda data: cast(int, data), value_fn=lambda data: cast(int, data),
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]}, attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
translation_key="uv_index",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="WetBulbTemperature", key="WetBulbTemperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="Wet bulb temperature",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="wet_bulb_temperature",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="WindChillTemperature", key="WindChillTemperature",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
name="Wind chill temperature",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="wind_chill_temperature",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="Wind", key="Wind",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
name="Wind",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
translation_key="wind_speed",
), ),
AccuWeatherSensorDescription( AccuWeatherSensorDescription(
key="WindGust", key="WindGust",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
name="Wind gust",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]), value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
translation_key="wind_gust_speed",
), ),
) )
@ -381,14 +467,12 @@ async def async_setup_entry(
] ]
if coordinator.forecast: if coordinator.forecast:
for description in FORECAST_SENSOR_TYPES:
# Some air quality/allergy sensors are only available for certain # Some air quality/allergy sensors are only available for certain
# locations. # locations.
sensors.extend( if description.key not in coordinator.data[ATTR_FORECAST][description.day]:
AccuWeatherSensor(coordinator, description, forecast_day=day) continue
for day in range(MAX_FORECAST_DAYS + 1) sensors.append(AccuWeatherSensor(coordinator, description))
for description in FORECAST_SENSOR_TYPES
if description.key in coordinator.data[ATTR_FORECAST][0]
)
async_add_entities(sensors) async_add_entities(sensors)
@ -406,25 +490,21 @@ class AccuWeatherSensor(
self, self,
coordinator: AccuWeatherDataUpdateCoordinator, coordinator: AccuWeatherDataUpdateCoordinator,
description: AccuWeatherSensorDescription, description: AccuWeatherSensorDescription,
forecast_day: int | None = None,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self.forecast_day = description.day
self.entity_description = description self.entity_description = description
self._sensor_data = _get_sensor_data( self._sensor_data = _get_sensor_data(
coordinator.data, description.key, forecast_day coordinator.data, description.key, self.forecast_day
)
if forecast_day is not None:
self._attr_name = f"{description.name} {forecast_day}d"
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}-{forecast_day}".lower()
) )
if self.forecast_day is not None:
self._attr_unique_id = f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower()
else: else:
self._attr_unique_id = ( self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}".lower() f"{coordinator.location_key}-{description.key}".lower()
) )
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
self.forecast_day = forecast_day
@property @property
def native_value(self) -> str | int | float | None: def native_value(self) -> str | int | float | None:

View File

@ -24,14 +24,8 @@
}, },
"entity": { "entity": {
"sensor": { "sensor": {
"pressure_tendency": { "air_quality_0d": {
"state": { "name": "Air quality today",
"steady": "Steady",
"rising": "Rising",
"falling": "Falling"
}
},
"air_quality": {
"state": { "state": {
"good": "Good", "good": "Good",
"hazardous": "Hazardous", "hazardous": "Hazardous",
@ -41,80 +35,761 @@
"unhealthy": "Unhealthy" "unhealthy": "Unhealthy"
} }
}, },
"grass_pollen": { "air_quality_1d": {
"name": "Air quality day 1",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
},
"air_quality_2d": {
"name": "Air quality day 2",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
},
"air_quality_3d": {
"name": "Air quality day 3",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
},
"air_quality_4d": {
"name": "Air quality day 4",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
},
"apparent_temperature": {
"name": "Apparent temperature"
},
"cloud_ceiling": {
"name": "Cloud ceiling"
},
"cloud_cover": {
"name": "Cloud cover"
},
"cloud_cover_day_0d": {
"name": "Cloud cover today"
},
"cloud_cover_day_1d": {
"name": "Cloud cover day 1"
},
"cloud_cover_day_2d": {
"name": "Cloud cover day 2"
},
"cloud_cover_day_3d": {
"name": "Cloud cover day 3"
},
"cloud_cover_day_4d": {
"name": "Cloud cover day 4"
},
"cloud_cover_night_0d": {
"name": "Cloud cover tonight"
},
"cloud_cover_night_1d": {
"name": "Cloud cover night 1"
},
"cloud_cover_night_2d": {
"name": "Cloud cover night 2"
},
"cloud_cover_night_3d": {
"name": "Cloud cover night 3"
},
"cloud_cover_night_4d": {
"name": "Cloud cover night 4"
},
"condition_day_0d": {
"name": "Condition today"
},
"condition_day_1d": {
"name": "Condition day 1"
},
"condition_day_2d": {
"name": "Condition day 2"
},
"condition_day_3d": {
"name": "Condition day 3"
},
"condition_day_4d": {
"name": "Condition day 4"
},
"condition_night_0d": {
"name": "Condition tonight"
},
"condition_night_1d": {
"name": "Condition night 1"
},
"condition_night_2d": {
"name": "Condition night 2"
},
"condition_night_3d": {
"name": "Condition night 3"
},
"condition_night_4d": {
"name": "Condition night 4"
},
"dew_point": {
"name": "Dew point"
},
"grass_pollen_0d": {
"name": "Grass pollen today",
"state_attributes": { "state_attributes": {
"level": { "level": {
"name": "Level", "name": "Level",
"state": { "state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
} }
} }
} }
}, },
"mold_pollen": { "grass_pollen_1d": {
"name": "Grass pollen day 1",
"state_attributes": { "state_attributes": {
"level": { "level": {
"name": "Level", "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": { "state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
} }
} }
} }
}, },
"ragweed_pollen": { "grass_pollen_2d": {
"name": "Grass pollen day 2",
"state_attributes": { "state_attributes": {
"level": { "level": {
"name": "Level", "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": { "state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
} }
} }
} }
}, },
"tree_pollen": { "grass_pollen_3d": {
"name": "Grass pollen day 3",
"state_attributes": { "state_attributes": {
"level": { "level": {
"name": "Level", "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": { "state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"grass_pollen_4d": {
"name": "Grass pollen day 4",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"hours_of_sun_0d": {
"name": "Hours of sun today"
},
"hours_of_sun_1d": {
"name": "Hours of sun day 1"
},
"hours_of_sun_2d": {
"name": "Hours of sun day 2"
},
"hours_of_sun_3d": {
"name": "Hours of sun day 3"
},
"hours_of_sun_4d": {
"name": "Hours of sun day 4"
},
"mold_pollen_0d": {
"name": "Mold pollen today",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"mold_pollen_1d": {
"name": "Mold pollen day 1",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"mold_pollen_2d": {
"name": "Mold pollen day 2",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"mold_pollen_3d": {
"name": "Mold pollen day 3",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"mold_pollen_4d": {
"name": "Mold pollen day 4",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"precipitation": {
"name": "[%key:component::sensor::entity_component::precipitation::name%]"
},
"pressure_tendency": {
"name": "Pressure tendency",
"state": {
"steady": "Steady",
"rising": "Rising",
"falling": "Falling"
}
},
"ragweed_pollen_0d": {
"name": "Ragweed pollen today",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"ragweed_pollen_1d": {
"name": "Ragweed pollen day 1",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"ragweed_pollen_2d": {
"name": "Ragweed pollen day 2",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"ragweed_pollen_3d": {
"name": "Ragweed pollen day 3",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"ragweed_pollen_4d": {
"name": "Ragweed pollen day 4",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"realfeel_temperature": {
"name": "RealFeel temperature"
},
"realfeel_temperature_max_0d": {
"name": "RealFeel temperature max today"
},
"realfeel_temperature_max_1d": {
"name": "RealFeel temperature max day 1"
},
"realfeel_temperature_max_2d": {
"name": "RealFeel temperature max day 2"
},
"realfeel_temperature_max_3d": {
"name": "RealFeel temperature max day 3"
},
"realfeel_temperature_max_4d": {
"name": "RealFeel temperature max day 4"
},
"realfeel_temperature_min_0d": {
"name": "RealFeel temperature min today"
},
"realfeel_temperature_min_1d": {
"name": "RealFeel temperature min day 1"
},
"realfeel_temperature_min_2d": {
"name": "RealFeel temperature min day 2"
},
"realfeel_temperature_min_3d": {
"name": "RealFeel temperature min day 3"
},
"realfeel_temperature_min_4d": {
"name": "RealFeel temperature min day 4"
},
"realfeel_temperature_shade": {
"name": "RealFeel temperature shade"
},
"realfeel_temperature_shade_max_0d": {
"name": "RealFeel temperature shade max today"
},
"realfeel_temperature_shade_max_1d": {
"name": "RealFeel temperature shade max day 1"
},
"realfeel_temperature_shade_max_2d": {
"name": "RealFeel temperature shade max day 2"
},
"realfeel_temperature_shade_max_3d": {
"name": "RealFeel temperature shade max day 3"
},
"realfeel_temperature_shade_max_4d": {
"name": "RealFeel temperature shade max day 4"
},
"realfeel_temperature_shade_min_0d": {
"name": "RealFeel temperature shade min today"
},
"realfeel_temperature_shade_min_1d": {
"name": "RealFeel temperature shade min day 1"
},
"realfeel_temperature_shade_min_2d": {
"name": "RealFeel temperature shade min day 2"
},
"realfeel_temperature_shade_min_3d": {
"name": "RealFeel temperature shade min day 3"
},
"realfeel_temperature_shade_min_4d": {
"name": "RealFeel temperature shade min day 4"
},
"solar_irradiance_day_0d": {
"name": "Solar irradiance today"
},
"solar_irradiance_day_1d": {
"name": "Solar irradiance day 1"
},
"solar_irradiance_day_2d": {
"name": "Solar irradiance day 2"
},
"solar_irradiance_day_3d": {
"name": "Solar irradiance day 3"
},
"solar_irradiance_day_4d": {
"name": "Solar irradiance day 4"
},
"solar_irradiance_night_0d": {
"name": "Solar irradiance tonight"
},
"solar_irradiance_night_1d": {
"name": "Solar irradiance night 1"
},
"solar_irradiance_night_2d": {
"name": "Solar irradiance night 2"
},
"solar_irradiance_night_3d": {
"name": "Solar irradiance night 3"
},
"solar_irradiance_night_4d": {
"name": "Solar irradiance night 4"
},
"thunderstorm_probability_day_0d": {
"name": "Thunderstorm probability today"
},
"thunderstorm_probability_day_1d": {
"name": "Thunderstorm probability day 1"
},
"thunderstorm_probability_day_2d": {
"name": "Thunderstorm probability day 2"
},
"thunderstorm_probability_day_3d": {
"name": "Thunderstorm probability day 3"
},
"thunderstorm_probability_day_4d": {
"name": "Thunderstorm probability day 4"
},
"thunderstorm_probability_night_0d": {
"name": "Thunderstorm probability tonight"
},
"thunderstorm_probability_night_1d": {
"name": "Thunderstorm probability night 1"
},
"thunderstorm_probability_night_2d": {
"name": "Thunderstorm probability night 2"
},
"thunderstorm_probability_night_3d": {
"name": "Thunderstorm probability night 3"
},
"thunderstorm_probability_night_4d": {
"name": "Thunderstorm probability night 4"
},
"tree_pollen_0d": {
"name": "Tree pollen today",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"tree_pollen_1d": {
"name": "Tree pollen day 1",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"tree_pollen_2d": {
"name": "Tree pollen day 2",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"tree_pollen_3d": {
"name": "Tree pollen day 3",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"tree_pollen_4d": {
"name": "Tree pollen day 4",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
} }
} }
} }
}, },
"uv_index": { "uv_index": {
"name": "UV index",
"state_attributes": { "state_attributes": {
"level": { "level": {
"name": "Level", "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": { "state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
} }
} }
} }
},
"uv_index_0d": {
"name": "UV index today",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"uv_index_1d": {
"name": "UV index day 1",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"uv_index_2d": {
"name": "UV index day 2",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"uv_index_3d": {
"name": "UV index day 3",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"uv_index_4d": {
"name": "UV index day 4",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"state": {
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
}
}
}
},
"wet_bulb_temperature": {
"name": "Wet bulb temperature"
},
"wind_speed": {
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]"
},
"wind_chill_temperature": {
"name": "Wind chill temperature"
},
"wind_gust_speed": {
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_gust_speed::name%]"
},
"wind_gust_speed_day_0d": {
"name": "Wind gust speed today"
},
"wind_gust_speed_day_1d": {
"name": "Wind gust speed day 1"
},
"wind_gust_speed_day_2d": {
"name": "Wind gust speed day 2"
},
"wind_gust_speed_day_3d": {
"name": "Wind gust speed day 3"
},
"wind_gust_speed_day_4d": {
"name": "Wind gust speed day 4"
},
"wind_gust_speed_night_0d": {
"name": "Wind gust speed tonight"
},
"wind_gust_speed_night_1d": {
"name": "Wind gust speed night 1"
},
"wind_gust_speed_night_2d": {
"name": "Wind gust speed night 2"
},
"wind_gust_speed_night_3d": {
"name": "Wind gust speed night 3"
},
"wind_gust_speed_night_4d": {
"name": "Wind gust speed night 4"
},
"wind_speed_day_0d": {
"name": "Wind speed today"
},
"wind_speed_day_1d": {
"name": "Wind speed day 1"
},
"wind_speed_day_2d": {
"name": "Wind speed day 2"
},
"wind_speed_day_3d": {
"name": "Wind speed day 3"
},
"wind_speed_day_4d": {
"name": "Wind speed day 4"
},
"wind_speed_night_0d": {
"name": "Wind speed tonight"
},
"wind_speed_night_1d": {
"name": "Wind speed night 1"
},
"wind_speed_night_2d": {
"name": "Wind speed night 2"
},
"wind_speed_night_3d": {
"name": "Wind speed night 3"
},
"wind_speed_night_4d": {
"name": "Wind speed night 4"
} }
} }
}, },

View File

@ -17,7 +17,8 @@ from homeassistant.components.weather import (
ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_UV_INDEX,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
Forecast, Forecast,
WeatherEntity, SingleCoordinatorWeatherEntity,
WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -27,9 +28,8 @@ from homeassistant.const import (
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherDataUpdateCoordinator from . import AccuWeatherDataUpdateCoordinator
@ -40,7 +40,7 @@ from .const import (
ATTR_SPEED, ATTR_SPEED,
ATTR_VALUE, ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
CONDITION_CLASSES, CONDITION_MAP,
DOMAIN, DOMAIN,
) )
@ -58,7 +58,7 @@ async def async_setup_entry(
class AccuWeatherEntity( class AccuWeatherEntity(
CoordinatorEntity[AccuWeatherDataUpdateCoordinator], WeatherEntity SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator]
): ):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
@ -68,9 +68,6 @@ class AccuWeatherEntity(
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
# Coordinator data is used also for sensors which don't have units automatically
# converted, hence the weather entity's native units follow the configured unit
# system
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_pressure_unit = UnitOfPressure.HPA
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
@ -79,18 +76,13 @@ class AccuWeatherEntity(
self._attr_unique_id = coordinator.location_key self._attr_unique_id = coordinator.location_key
self._attr_attribution = ATTRIBUTION self._attr_attribution = ATTRIBUTION
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
if self.coordinator.forecast:
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
@property @property
def condition(self) -> str | None: def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
try: return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"])
return [
k
for k, v in CONDITION_CLASSES.items()
if self.coordinator.data["WeatherIcon"] in v
][0]
except IndexError:
return None
@property @property
def cloud_coverage(self) -> float: def cloud_coverage(self) -> float:
@ -180,9 +172,12 @@ class AccuWeatherEntity(
], ],
ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE], ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE],
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: [ ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]),
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
][0],
} }
for item in self.coordinator.data[ATTR_FORECAST] for item in self.coordinator.data[ATTR_FORECAST]
] ]
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast in native units."""
return self.forecast

View File

@ -74,9 +74,9 @@ class AcmedaBase(entity.Entity):
return self.roller.id return self.roller.id
@property @property
def device_info(self) -> entity.DeviceInfo: def device_info(self) -> dr.DeviceInfo:
"""Return the device info.""" """Return the device info."""
return entity.DeviceInfo( return dr.DeviceInfo(
identifiers={(DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Rollease Acmeda", manufacturer="Rollease Acmeda",
name=self.roller.name, name=self.roller.name,

View File

@ -2,11 +2,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from contextlib import suppress from contextlib import suppress
from typing import Any from typing import Any
import aiopulse import aiopulse
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -43,7 +43,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
hubs: list[aiopulse.Hub] = [] hubs: list[aiopulse.Hub] = []
with suppress(asyncio.TimeoutError): with suppress(asyncio.TimeoutError):
async with async_timeout.timeout(5): async with timeout(5):
async for hub in aiopulse.Hub.discover(): async for hub in aiopulse.Hub.discover():
if hub.id not in already_configured: if hub.id not in already_configured:
hubs.append(hub) hubs.append(hub)

View File

@ -23,7 +23,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL

View File

@ -4,8 +4,8 @@ from __future__ import annotations
from adguardhome import AdGuardHome, AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeError
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import Entity
from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER

View File

@ -1,12 +1,12 @@
"""Support for Automation Device Specification (ADS).""" """Support for Automation Device Specification (ADS)."""
import asyncio import asyncio
from asyncio import timeout
from collections import namedtuple from collections import namedtuple
import ctypes import ctypes
import logging import logging
import struct import struct
import threading import threading
import async_timeout
import pyads import pyads
import voluptuous as vol import voluptuous as vol
@ -301,7 +301,7 @@ class AdsEntity(Entity):
self._ads_hub.add_device_notification, ads_var, plctype, update self._ads_hub.add_device_notification, ads_var, plctype, update
) )
try: try:
async with async_timeout.timeout(10): async with timeout(10):
await self._event.wait() await self._event.wait()
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.debug("Variable %s: Timeout during first update", ads_var) _LOGGER.debug("Variable %s: Timeout during first update", ads_var)

View File

@ -4,7 +4,7 @@ from typing import Any
from advantage_air import ApiError from advantage_air import ApiError
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import DOMAIN

View File

@ -4,7 +4,7 @@ from typing import Any
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN

View File

@ -3,7 +3,7 @@
from homeassistant.components.update import UpdateEntity from homeassistant.components.update import UpdateEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN

View File

@ -1,11 +1,14 @@
"""The AEMET OpenData component.""" """The AEMET OpenData component."""
import logging import logging
from aemet_opendata.interface import AEMET from aemet_opendata.exceptions import TownNotFound
from aemet_opendata.interface import AEMET, ConnectionOptions
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from .const import ( from .const import (
CONF_STATION_UPDATES, CONF_STATION_UPDATES,
@ -27,11 +30,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
longitude = entry.data[CONF_LONGITUDE] longitude = entry.data[CONF_LONGITUDE]
station_updates = entry.options.get(CONF_STATION_UPDATES, True) station_updates = entry.options.get(CONF_STATION_UPDATES, True)
aemet = AEMET(api_key) options = ConnectionOptions(api_key, station_updates)
weather_coordinator = WeatherUpdateCoordinator( aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
hass, aemet, latitude, longitude, station_updates try:
) await aemet.select_coordinates(latitude, longitude)
except TownNotFound as err:
_LOGGER.error(err)
return False
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
await weather_coordinator.async_config_entry_first_refresh() await weather_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})

View File

@ -1,13 +1,14 @@
"""Config flow for AEMET OpenData.""" """Config flow for AEMET OpenData."""
from __future__ import annotations from __future__ import annotations
from aemet_opendata import AEMET from aemet_opendata.exceptions import AuthError
from aemet_opendata.interface import AEMET, ConnectionOptions
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import ( from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep, SchemaFlowFormStep,
SchemaOptionsFlowHandler, SchemaOptionsFlowHandler,
@ -39,8 +40,11 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(f"{latitude}-{longitude}") await self.async_set_unique_id(f"{latitude}-{longitude}")
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY]) options = ConnectionOptions(user_input[CONF_API_KEY], False)
if not api_online: aemet = AEMET(aiohttp_client.async_get_clientsession(self.hass), options)
try:
await aemet.select_coordinates(latitude, longitude)
except AuthError:
errors["base"] = "invalid_api_key" errors["base"] = "invalid_api_key"
if not errors: if not errors:
@ -70,10 +74,3 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> SchemaOptionsFlowHandler: ) -> SchemaOptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
async def _is_aemet_api_online(hass, api_key):
aemet = AEMET(api_key)
return await hass.async_add_executor_job(
aemet.get_conventional_observation_stations, False
)

View File

@ -33,6 +33,7 @@ ATTR_API_FORECAST_TEMP = "temperature"
ATTR_API_FORECAST_TEMP_LOW = "templow" ATTR_API_FORECAST_TEMP_LOW = "templow"
ATTR_API_FORECAST_TIME = "datetime" ATTR_API_FORECAST_TIME = "datetime"
ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" ATTR_API_FORECAST_WIND_BEARING = "wind_bearing"
ATTR_API_FORECAST_WIND_MAX_SPEED = "wind_max_speed"
ATTR_API_FORECAST_WIND_SPEED = "wind_speed" ATTR_API_FORECAST_WIND_SPEED = "wind_speed"
ATTR_API_HUMIDITY = "humidity" ATTR_API_HUMIDITY = "humidity"
ATTR_API_PRESSURE = "pressure" ATTR_API_PRESSURE = "pressure"

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet", "documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aemet_opendata"], "loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.2.2"] "requirements": ["AEMET-OpenData==0.4.4"]
} }

View File

@ -1,14 +1,20 @@
"""Support for the AEMET OpenData service.""" """Support for the AEMET OpenData service."""
from typing import cast
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
WeatherEntity, DOMAIN as WEATHER_DOMAIN,
Forecast,
SingleCoordinatorWeatherEntity,
WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -17,9 +23,9 @@ from homeassistant.const import (
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import (
ATTR_API_CONDITION, ATTR_API_CONDITION,
@ -30,6 +36,7 @@ from .const import (
ATTR_API_FORECAST_TEMP_LOW, ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME, ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING, ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED,
ATTR_API_FORECAST_WIND_SPEED, ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY, ATTR_API_HUMIDITY,
ATTR_API_PRESSURE, ATTR_API_PRESSURE,
@ -64,6 +71,7 @@ FORECAST_MAP = {
ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP,
ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED: ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED,
}, },
} }
@ -79,15 +87,33 @@ async def async_setup_entry(
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
entities = [] entities = []
entity_registry = er.async_get(hass)
# Add daily + hourly entity for legacy config entries, only add daily for new
# config entries. This can be removed in HA Core 2024.3
if entity_registry.async_get_entity_id(
WEATHER_DOMAIN,
DOMAIN,
f"{config_entry.unique_id} {FORECAST_MODE_HOURLY}",
):
for mode in FORECAST_MODES: for mode in FORECAST_MODES:
name = f"{domain_data[ENTRY_NAME]} {mode}" name = f"{domain_data[ENTRY_NAME]} {mode}"
unique_id = f"{config_entry.unique_id} {mode}" unique_id = f"{config_entry.unique_id} {mode}"
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
else:
entities.append(
AemetWeather(
domain_data[ENTRY_NAME],
config_entry.unique_id,
weather_coordinator,
FORECAST_MODE_DAILY,
)
)
async_add_entities(entities, False) async_add_entities(entities, False)
class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): class AemetWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
"""Implementation of an AEMET OpenData sensor.""" """Implementation of an AEMET OpenData sensor."""
_attr_attribution = ATTRIBUTION _attr_attribution = ATTRIBUTION
@ -95,6 +121,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
_attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_pressure_unit = UnitOfPressure.HPA
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_supported_features = (
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
)
def __init__( def __init__(
self, self,
@ -117,15 +146,32 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
"""Return the current condition.""" """Return the current condition."""
return self.coordinator.data[ATTR_API_CONDITION] return self.coordinator.data[ATTR_API_CONDITION]
@property def _forecast(self, forecast_mode: str) -> list[Forecast]:
def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[forecast_mode]]
forecast_map = FORECAST_MAP[self._forecast_mode] forecast_map = FORECAST_MAP[forecast_mode]
return [ return cast(
list[Forecast],
[
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
for forecast in forecasts for forecast in forecasts
] ],
)
@property
def forecast(self) -> list[Forecast]:
"""Return the forecast array."""
return self._forecast(self._forecast_mode)
@callback
def _async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast(FORECAST_MODE_DAILY)
@callback
def _async_forecast_hourly(self) -> list[Forecast]:
"""Return the hourly forecast in native units."""
return self._forecast(FORECAST_MODE_HOURLY)
@property @property
def humidity(self): def humidity(self):

View File

@ -1,22 +1,21 @@
"""Weather data coordinator for the AEMET OpenData service.""" """Weather data coordinator for the AEMET OpenData service."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any, Final
from aemet_opendata.const import ( from aemet_opendata.const import (
AEMET_ATTR_DATE, AEMET_ATTR_DATE,
AEMET_ATTR_DAY, AEMET_ATTR_DAY,
AEMET_ATTR_DIRECTION, AEMET_ATTR_DIRECTION,
AEMET_ATTR_ELABORATED, AEMET_ATTR_ELABORATED,
AEMET_ATTR_FEEL_TEMPERATURE,
AEMET_ATTR_FORECAST, AEMET_ATTR_FORECAST,
AEMET_ATTR_HUMIDITY, AEMET_ATTR_HUMIDITY,
AEMET_ATTR_ID,
AEMET_ATTR_IDEMA,
AEMET_ATTR_MAX, AEMET_ATTR_MAX,
AEMET_ATTR_MIN, AEMET_ATTR_MIN,
AEMET_ATTR_NAME,
AEMET_ATTR_PRECIPITATION, AEMET_ATTR_PRECIPITATION,
AEMET_ATTR_PRECIPITATION_PROBABILITY, AEMET_ATTR_PRECIPITATION_PROBABILITY,
AEMET_ATTR_SKY_STATE, AEMET_ATTR_SKY_STATE,
@ -25,24 +24,24 @@ from aemet_opendata.const import (
AEMET_ATTR_SPEED, AEMET_ATTR_SPEED,
AEMET_ATTR_STATION_DATE, AEMET_ATTR_STATION_DATE,
AEMET_ATTR_STATION_HUMIDITY, AEMET_ATTR_STATION_HUMIDITY,
AEMET_ATTR_STATION_LOCATION,
AEMET_ATTR_STATION_PRESSURE, AEMET_ATTR_STATION_PRESSURE,
AEMET_ATTR_STATION_PRESSURE_SEA, AEMET_ATTR_STATION_PRESSURE_SEA,
AEMET_ATTR_STATION_TEMPERATURE, AEMET_ATTR_STATION_TEMPERATURE,
AEMET_ATTR_STORM_PROBABILITY, AEMET_ATTR_STORM_PROBABILITY,
AEMET_ATTR_TEMPERATURE, AEMET_ATTR_TEMPERATURE,
AEMET_ATTR_TEMPERATURE_FEELING,
AEMET_ATTR_WIND, AEMET_ATTR_WIND,
AEMET_ATTR_WIND_GUST, AEMET_ATTR_WIND_GUST,
ATTR_DATA, ATTR_DATA,
) )
from aemet_opendata.exceptions import AemetError
from aemet_opendata.helpers import ( from aemet_opendata.helpers import (
get_forecast_day_value, get_forecast_day_value,
get_forecast_hour_value, get_forecast_hour_value,
get_forecast_interval_value, get_forecast_interval_value,
) )
import async_timeout from aemet_opendata.interface import AEMET
from homeassistant.core import HomeAssistant
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
@ -57,6 +56,7 @@ from .const import (
ATTR_API_FORECAST_TEMP_LOW, ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME, ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING, ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED,
ATTR_API_FORECAST_WIND_SPEED, ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY, ATTR_API_HUMIDITY,
ATTR_API_PRESSURE, ATTR_API_PRESSURE,
@ -83,6 +83,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
API_TIMEOUT: Final[int] = 120
STATION_MAX_DELTA = timedelta(hours=2) STATION_MAX_DELTA = timedelta(hours=2)
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
@ -112,128 +113,33 @@ def format_int(value) -> int | None:
return None return None
class TownNotFound(UpdateFailed):
"""Raised when town is not found."""
class WeatherUpdateCoordinator(DataUpdateCoordinator): class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator.""" """Weather data update coordinator."""
def __init__(self, hass, aemet, latitude, longitude, station_updates): def __init__(
self,
hass: HomeAssistant,
aemet: AEMET,
) -> None:
"""Initialize coordinator.""" """Initialize coordinator."""
self.aemet = aemet
super().__init__( super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL hass,
_LOGGER,
name=DOMAIN,
update_interval=WEATHER_UPDATE_INTERVAL,
) )
self._aemet = aemet async def _async_update_data(self) -> dict[str, Any]:
self._station = None """Update coordinator data."""
self._town = None async with timeout(API_TIMEOUT):
self._latitude = latitude try:
self._longitude = longitude await self.aemet.update()
self._station_updates = station_updates except AemetError as error:
self._data = { raise UpdateFailed(error) from error
"daily": None, weather_response = self.aemet.legacy_weather()
"hourly": None, return self._convert_weather_response(weather_response)
"station": None,
}
async def _async_update_data(self):
data = {}
async with async_timeout.timeout(120):
weather_response = await self._get_aemet_weather()
data = self._convert_weather_response(weather_response)
return data
async def _get_aemet_weather(self):
"""Poll weather data from AEMET OpenData."""
weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast)
return weather
def _get_weather_station(self):
if not self._station:
self._station = (
self._aemet.get_conventional_observation_station_by_coordinates(
self._latitude, self._longitude
)
)
if self._station:
_LOGGER.debug(
"station found for coordinates [%s, %s]: %s",
self._latitude,
self._longitude,
self._station,
)
if not self._station:
_LOGGER.debug(
"station not found for coordinates [%s, %s]",
self._latitude,
self._longitude,
)
return self._station
def _get_weather_town(self):
if not self._town:
self._town = self._aemet.get_town_by_coordinates(
self._latitude, self._longitude
)
if self._town:
_LOGGER.debug(
"Town found for coordinates [%s, %s]: %s",
self._latitude,
self._longitude,
self._town,
)
if not self._town:
_LOGGER.error(
"Town not found for coordinates [%s, %s]",
self._latitude,
self._longitude,
)
raise TownNotFound
return self._town
def _get_weather_and_forecast(self):
"""Get weather and forecast data from AEMET OpenData."""
self._get_weather_town()
daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID])
if not daily:
_LOGGER.error(
'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID]
)
hourly = self._aemet.get_specific_forecast_town_hourly(
self._town[AEMET_ATTR_ID]
)
if not hourly:
_LOGGER.error(
'Error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID]
)
station = None
if self._station_updates and self._get_weather_station():
station = self._aemet.get_conventional_observation_station_data(
self._station[AEMET_ATTR_IDEMA]
)
if not station:
_LOGGER.error(
'Error fetching data for station "%s"',
self._station[AEMET_ATTR_IDEMA],
)
if daily:
self._data["daily"] = daily
if hourly:
self._data["hourly"] = hourly
if station:
self._data["station"] = station
return AemetWeather(
self._data["daily"],
self._data["hourly"],
self._data["station"],
)
def _convert_weather_response(self, weather_response): def _convert_weather_response(self, weather_response):
"""Format the weather response correctly.""" """Format the weather response correctly."""
@ -428,6 +334,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
), ),
ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour),
ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(),
ATTR_API_FORECAST_WIND_MAX_SPEED: self._get_wind_max_speed(day, hour),
ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
} }
@ -518,14 +425,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
def _get_station_id(self): def _get_station_id(self):
"""Get station ID from weather data.""" """Get station ID from weather data."""
if self._station: if self.aemet.station:
return self._station[AEMET_ATTR_IDEMA] return self.aemet.station.get_id()
return None return None
def _get_station_name(self): def _get_station_name(self):
"""Get station name from weather data.""" """Get station name from weather data."""
if self._station: if self.aemet.station:
return self._station[AEMET_ATTR_STATION_LOCATION] return self.aemet.station.get_name()
return None return None
@staticmethod @staticmethod
@ -561,19 +468,19 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
@staticmethod @staticmethod
def _get_temperature_feeling(day_data, hour): def _get_temperature_feeling(day_data, hour):
"""Get temperature from weather data.""" """Get temperature from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour) val = get_forecast_hour_value(day_data[AEMET_ATTR_FEEL_TEMPERATURE], hour)
return format_int(val) return format_int(val)
def _get_town_id(self): def _get_town_id(self):
"""Get town ID from weather data.""" """Get town ID from weather data."""
if self._town: if self.aemet.town:
return self._town[AEMET_ATTR_ID] return self.aemet.town.get_id()
return None return None
def _get_town_name(self): def _get_town_name(self):
"""Get town name from weather data.""" """Get town name from weather data."""
if self._town: if self.aemet.town:
return self._town[AEMET_ATTR_NAME] return self.aemet.town.get_name()
return None return None
@staticmethod @staticmethod
@ -623,12 +530,3 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
if val: if val:
return format_int(val) return format_int(val)
return None return None
@dataclass
class AemetWeather:
"""Class to harmonize weather data model."""
daily: dict = field(default_factory=dict)
hourly: dict = field(default_factory=dict)
station: dict = field(default_factory=dict)

View File

@ -13,7 +13,7 @@ from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN from .const import CONNECTION, DOMAIN as AGENT_DOMAIN

View File

@ -8,7 +8,7 @@ from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import (
AddEntitiesCallback, AddEntitiesCallback,
async_get_current_platform, async_get_current_platform,

View File

@ -1,6 +1,7 @@
"""The Airly integration.""" """The Airly integration."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from math import ceil from math import ceil
@ -9,7 +10,6 @@ from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from airly import Airly from airly import Airly
from airly.exceptions import AirlyError from airly.exceptions import AirlyError
import async_timeout
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -167,7 +167,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
measurements = self.airly.create_measurements_session_point( measurements = self.airly.create_measurements_session_point(
self.latitude, self.longitude self.latitude, self.longitude
) )
async with async_timeout.timeout(20): async with timeout(20):
try: try:
await measurements.update() await measurements.update()
except (AirlyError, ClientConnectorError) as error: except (AirlyError, ClientConnectorError) as error:

View File

@ -1,13 +1,13 @@
"""Adds config flow for Airly.""" """Adds config flow for Airly."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from http import HTTPStatus from http import HTTPStatus
from typing import Any from typing import Any
from aiohttp import ClientSession from aiohttp import ClientSession
from airly import Airly from airly import Airly
from airly.exceptions import AirlyError from airly.exceptions import AirlyError
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -105,7 +105,7 @@ async def test_location(
measurements = airly.create_measurements_session_point( measurements = airly.create_measurements_session_point(
latitude=latitude, longitude=longitude latitude=latitude, longitude=longitude
) )
async with async_timeout.timeout(10): async with timeout(10):
await measurements.update() await measurements.update()
current = measurements.current current = measurements.current

View File

@ -20,8 +20,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
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

View File

@ -47,7 +47,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
latitude = entry.data[CONF_LATITUDE] latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE] longitude = entry.data[CONF_LONGITUDE]
distance = entry.data[CONF_RADIUS]
# Station Radius is a user-configurable option
distance = entry.options[CONF_RADIUS]
# Reports are published hourly but update twice per hour # Reports are published hourly but update twice per hour
update_interval = datetime.timedelta(minutes=30) update_interval = datetime.timedelta(minutes=30)
@ -65,11 +67,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
# Listen for option changes
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", entry.version)
if entry.version == 1:
new_options = {CONF_RADIUS: entry.data[CONF_RADIUS]}
new_data = entry.data.copy()
del new_data[CONF_RADIUS]
entry.version = 2
hass.config_entries.async_update_entry(
entry, data=new_data, options=new_options
)
_LOGGER.info("Migration to version %s successful", entry.version)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@ -80,6 +104,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
class AirNowDataUpdateCoordinator(DataUpdateCoordinator): class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Airly data.""" """Define an object to hold Airly data."""

View File

@ -1,11 +1,12 @@
"""Config flow for AirNow integration.""" """Config flow for AirNow integration."""
import logging import logging
from typing import Any
from pyairnow import WebServiceAPI from pyairnow import WebServiceAPI
from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, data_entry_flow, exceptions
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -48,7 +49,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AirNow.""" """Handle a config flow for AirNow."""
VERSION = 1 VERSION = 2
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle the initial step.""" """Handle the initial step."""
@ -75,12 +76,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
# Create Entry # Create Entry
radius = user_input.pop(CONF_RADIUS)
return self.async_create_entry( return self.async_create_entry(
title=( title=(
f"AirNow Sensor at {user_input[CONF_LATITUDE]}," f"AirNow Sensor at {user_input[CONF_LATITUDE]},"
f" {user_input[CONF_LONGITUDE]}" f" {user_input[CONF_LONGITUDE]}"
), ),
data=user_input, data=user_input,
options={CONF_RADIUS: radius},
) )
return self.async_show_form( return self.async_show_form(
@ -94,12 +97,49 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
vol.Optional( vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude, ): cv.longitude,
vol.Optional(CONF_RADIUS, default=150): int, vol.Optional(CONF_RADIUS, default=150): vol.All(
int, vol.Range(min=5)
),
} }
), ),
errors=errors, errors=errors,
) )
@staticmethod
@core.callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Return the options flow."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
"""Handle an options flow for AirNow."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> data_entry_flow.FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(data=user_input)
options_schema = vol.Schema(
{
vol.Optional(CONF_RADIUS): vol.All(
int,
vol.Range(min=5),
),
}
)
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
options_schema, self.config_entry.options
),
)
class CannotConnect(exceptions.HomeAssistantError): class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect.""" """Error to indicate we cannot connect."""

View File

@ -17,8 +17,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -30,6 +29,9 @@ from .const import (
ATTR_API_AQI_LEVEL, ATTR_API_AQI_LEVEL,
ATTR_API_O3, ATTR_API_O3,
ATTR_API_PM25, ATTR_API_PM25,
ATTR_API_STATION,
ATTR_API_STATION_LATITUDE,
ATTR_API_STATION_LONGITUDE,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
) )
@ -40,6 +42,7 @@ PARALLEL_UPDATES = 1
ATTR_DESCR = "description" ATTR_DESCR = "description"
ATTR_LEVEL = "level" ATTR_LEVEL = "level"
ATTR_STATION = "reporting_station"
@dataclass @dataclass
@ -85,6 +88,16 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
value_fn=lambda data: data.get(ATTR_API_O3), value_fn=lambda data: data.get(ATTR_API_O3),
extra_state_attributes_fn=None, extra_state_attributes_fn=None,
), ),
AirNowEntityDescription(
key=ATTR_API_STATION,
translation_key="station",
icon="mdi:blur",
value_fn=lambda data: data.get(ATTR_API_STATION),
extra_state_attributes_fn=lambda data: {
"lat": data[ATTR_API_STATION_LATITUDE],
"long": data[ATTR_API_STATION_LONGITUDE],
},
),
) )

View File

@ -21,10 +21,26 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
} }
}, },
"options": {
"step": {
"init": {
"data": {
"radius": "Station Radius (miles)"
}
}
}
},
"entity": { "entity": {
"sensor": { "sensor": {
"o3": { "o3": {
"name": "[%key:component::sensor::entity_component::ozone::name%]" "name": "[%key:component::sensor::entity_component::ozone::name%]"
},
"station": {
"name": "PM2.5 reporting station",
"state_attributes": {
"lat": { "name": "[%key:common::config_flow::data::latitude%]" },
"long": { "name": "[%key:common::config_flow::data::longitude%]" }
}
} }
} }
} }

View File

@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL

View File

@ -21,7 +21,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (

View File

@ -34,8 +34,12 @@ class Discovery:
def get_name(device: AirthingsDevice) -> str: def get_name(device: AirthingsDevice) -> str:
"""Generate name with identifier for device.""" """Generate name with model and identifier for device."""
return f"{device.name} ({device.identifier})"
name = device.friendly_name()
if identifier := device.identifier:
name += f" ({identifier})"
return name
class AirthingsDeviceUpdateError(Exception): class AirthingsDeviceUpdateError(Exception):
@ -156,7 +160,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="no_devices_found") return self.async_abort(reason="no_devices_found")
titles = { titles = {
address: get_name(discovery.device) address: discovery.device.name
for (address, discovery) in self._discovered_devices.items() for (address, discovery) in self._discovered_devices.items()
} }
return self.async_show_form( return self.async_show_form(

View File

@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble", "documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["airthings-ble==0.5.3"] "requirements": ["airthings-ble==0.5.6-2"]
} }

View File

@ -22,8 +22,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
@ -162,11 +161,11 @@ class AirthingsSensor(
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = entity_description self.entity_description = entity_description
name = f"{airthings_device.name} {airthings_device.identifier}" name = airthings_device.name
if identifier := airthings_device.identifier:
name += f" ({identifier})"
self._attr_unique_id = f"{name}_{entity_description.key}" self._attr_unique_id = f"{name}_{entity_description.key}"
self._id = airthings_device.address
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
connections={ connections={
( (
@ -175,9 +174,10 @@ class AirthingsSensor(
) )
}, },
name=name, name=name,
manufacturer="Airthings", manufacturer=airthings_device.manufacturer,
hw_version=airthings_device.hw_version, hw_version=airthings_device.hw_version,
sw_version=airthings_device.sw_version, sw_version=airthings_device.sw_version,
model=airthings_device.model,
) )
@property @property

View File

@ -18,7 +18,7 @@ from homeassistant.components.climate import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
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
@ -98,28 +98,20 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
self._ac_number = ac_number self._ac_number = ac_number
self._airtouch = coordinator.airtouch self._airtouch = coordinator.airtouch
self._info = info self._info = info
self._unit = self._airtouch.GetAcs()[self._ac_number] self._unit = self._airtouch.GetAcs()[ac_number]
self._attr_unique_id = f"ac_{ac_number}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"ac_{ac_number}")},
name=f"AC {ac_number}",
manufacturer="Airtouch",
model="Airtouch 4",
)
@callback @callback
def _handle_coordinator_update(self): def _handle_coordinator_update(self):
self._unit = self._airtouch.GetAcs()[self._ac_number] self._unit = self._airtouch.GetAcs()[self._ac_number]
return super()._handle_coordinator_update() return super()._handle_coordinator_update()
@property
def device_info(self) -> DeviceInfo:
"""Return device info for this device."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
name=f"AC {self._ac_number}",
manufacturer="Airtouch",
model="Airtouch 4",
)
@property
def unique_id(self):
"""Return unique ID for this device."""
return f"ac_{self._ac_number}"
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
@ -208,29 +200,21 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
"""Initialize the climate device.""" """Initialize the climate device."""
super().__init__(coordinator) super().__init__(coordinator)
self._group_number = group_number self._group_number = group_number
self._attr_unique_id = group_number
self._airtouch = coordinator.airtouch self._airtouch = coordinator.airtouch
self._info = info self._info = info
self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) self._unit = self._airtouch.GetGroupByGroupNumber(group_number)
self._attr_device_info = DeviceInfo(
@callback identifiers={(DOMAIN, group_number)},
def _handle_coordinator_update(self):
self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number)
return super()._handle_coordinator_update()
@property
def device_info(self) -> DeviceInfo:
"""Return device info for this device."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
manufacturer="Airtouch", manufacturer="Airtouch",
model="Airtouch 4", model="Airtouch 4",
name=self._unit.GroupName, name=self._unit.GroupName,
) )
@property @callback
def unique_id(self): def _handle_coordinator_update(self):
"""Return unique ID for this device.""" self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number)
return self._group_number return super()._handle_coordinator_update()
@property @property
def min_temp(self): def min_temp(self):

View File

@ -1,7 +1,7 @@
{ {
"domain": "airtouch4", "domain": "airtouch4",
"name": "AirTouch 4", "name": "AirTouch 4",
"codeowners": ["@LonePurpleWolf"], "codeowners": [],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airtouch4", "documentation": "https://www.home-assistant.io/integrations/airtouch4",
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -109,19 +109,21 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"airvisual_checked_api_keys_lock", asyncio.Lock() "airvisual_checked_api_keys_lock", asyncio.Lock()
) )
async with valid_keys_lock:
if user_input[CONF_API_KEY] not in valid_keys:
if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS: if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
coro = cloud_api.air_quality.nearest_city() coro = cloud_api.air_quality.nearest_city()
error_schema = self.geography_coords_schema error_schema = self.geography_coords_schema
error_step = "geography_by_coords" error_step = "geography_by_coords"
else: else:
coro = cloud_api.air_quality.city( coro = cloud_api.air_quality.city(
user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY] user_input[CONF_CITY],
user_input[CONF_STATE],
user_input[CONF_COUNTRY],
) )
error_schema = GEOGRAPHY_NAME_SCHEMA error_schema = GEOGRAPHY_NAME_SCHEMA
error_step = "geography_by_name" error_step = "geography_by_name"
async with valid_keys_lock:
if user_input[CONF_API_KEY] not in valid_keys:
try: try:
await coro await coro
except (InvalidKeyError, KeyExpiredError, UnauthorizedError): except (InvalidKeyError, KeyExpiredError, UnauthorizedError):

View File

@ -23,7 +23,8 @@ from homeassistant.const import (
) )
from homeassistant.core import Event, HomeAssistant from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,

View File

@ -1,13 +1,13 @@
"""The Airzone integration.""" """The Airzone integration."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from aioairzone.exceptions import AirzoneError from aioairzone.exceptions import AirzoneError
from aioairzone.localapi import AirzoneLocalApi from aioairzone.localapi import AirzoneLocalApi
import async_timeout
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -35,7 +35,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): async with timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
try: try:
await self.airzone.update() await self.airzone.update()
except AirzoneError as error: except AirzoneError as error:

View File

@ -10,6 +10,7 @@ from aioairzone.const import (
AZD_AVAILABLE, AZD_AVAILABLE,
AZD_FIRMWARE, AZD_FIRMWARE,
AZD_FULL_NAME, AZD_FULL_NAME,
AZD_HOT_WATER,
AZD_ID, AZD_ID,
AZD_MAC, AZD_MAC,
AZD_MODEL, AZD_MODEL,
@ -26,7 +27,7 @@ from aioairzone.exceptions import AirzoneError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER from .const import DOMAIN, MANUFACTURER
@ -81,6 +82,31 @@ class AirzoneSystemEntity(AirzoneEntity):
return value return value
class AirzoneHotWaterEntity(AirzoneEntity):
"""Define an Airzone Hot Water entity."""
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
entry: ConfigEntry,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.entry_id}_dhw")},
manufacturer=MANUFACTURER,
model="DHW",
name=self.get_airzone_value(AZD_NAME),
via_device=(DOMAIN, f"{entry.entry_id}_ws"),
)
self._attr_unique_id = entry.unique_id or entry.entry_id
def get_airzone_value(self, key: str) -> Any:
"""Return DHW value by key."""
return self.coordinator.data[AZD_HOT_WATER].get(key)
class AirzoneWebServerEntity(AirzoneEntity): class AirzoneWebServerEntity(AirzoneEntity):
"""Define an Airzone WebServer entity.""" """Define an Airzone WebServer entity."""

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.5"] "requirements": ["aioairzone==0.6.8"]
} }

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any, Final from typing import Any, Final
from aioairzone.const import ( from aioairzone.const import (
AZD_HOT_WATER,
AZD_HUMIDITY, AZD_HUMIDITY,
AZD_NAME, AZD_NAME,
AZD_TEMP, AZD_TEMP,
@ -31,7 +32,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS
from .coordinator import AirzoneUpdateCoordinator from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneWebServerEntity, AirzoneZoneEntity from .entity import (
AirzoneEntity,
AirzoneHotWaterEntity,
AirzoneWebServerEntity,
AirzoneZoneEntity,
)
HOT_WATER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription(
device_class=SensorDeviceClass.TEMPERATURE,
key=AZD_TEMP,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
)
WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription( SensorEntityDescription(
@ -71,6 +86,18 @@ async def async_setup_entry(
sensors: list[AirzoneSensor] = [] sensors: list[AirzoneSensor] = []
if AZD_HOT_WATER in coordinator.data:
dhw_data = coordinator.data[AZD_HOT_WATER]
for description in HOT_WATER_SENSOR_TYPES:
if description.key in dhw_data:
sensors.append(
AirzoneHotWaterSensor(
coordinator,
description,
entry,
)
)
if AZD_WEBSERVER in coordinator.data: if AZD_WEBSERVER in coordinator.data:
ws_data = coordinator.data[AZD_WEBSERVER] ws_data = coordinator.data[AZD_WEBSERVER]
for description in WEBSERVER_SENSOR_TYPES: for description in WEBSERVER_SENSOR_TYPES:
@ -114,6 +141,30 @@ class AirzoneSensor(AirzoneEntity, SensorEntity):
self._attr_native_value = self.get_airzone_value(self.entity_description.key) self._attr_native_value = self.get_airzone_value(self.entity_description.key)
class AirzoneHotWaterSensor(AirzoneHotWaterEntity, AirzoneSensor):
"""Define an Airzone Hot Water sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: SensorEntityDescription,
entry: ConfigEntry,
) -> None:
"""Initialize."""
super().__init__(coordinator, entry)
self._attr_unique_id = f"{self._attr_unique_id}_dhw_{description.key}"
self.entity_description = description
self._attr_native_unit_of_measurement = TEMP_UNIT_LIB_TO_HASS.get(
self.get_airzone_value(AZD_TEMP_UNIT)
)
self._async_update_attrs()
class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
"""Define an Airzone WebServer sensor.""" """Define an Airzone WebServer sensor."""

View File

@ -9,6 +9,7 @@ from aioairzone_cloud.const import (
AZD_AIDOOS, AZD_AIDOOS,
AZD_ERRORS, AZD_ERRORS,
AZD_PROBLEMS, AZD_PROBLEMS,
AZD_SYSTEMS,
AZD_WARNINGS, AZD_WARNINGS,
AZD_ZONES, AZD_ZONES,
) )
@ -25,7 +26,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import AirzoneUpdateCoordinator from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity from .entity import (
AirzoneAidooEntity,
AirzoneEntity,
AirzoneSystemEntity,
AirzoneZoneEntity,
)
@dataclass @dataclass
@ -51,6 +57,20 @@ AIDOO_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...
), ),
) )
SYSTEM_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
AirzoneBinarySensorEntityDescription(
attributes={
"errors": AZD_ERRORS,
"warnings": AZD_WARNINGS,
},
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
key=AZD_PROBLEMS,
),
)
ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
AirzoneBinarySensorEntityDescription( AirzoneBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.RUNNING, device_class=BinarySensorDeviceClass.RUNNING,
@ -87,6 +107,18 @@ async def async_setup_entry(
) )
) )
for system_id, system_data in coordinator.data.get(AZD_SYSTEMS, {}).items():
for description in SYSTEM_BINARY_SENSOR_TYPES:
if description.key in system_data:
binary_sensors.append(
AirzoneSystemBinarySensor(
coordinator,
description,
system_id,
system_data,
)
)
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items():
for description in ZONE_BINARY_SENSOR_TYPES: for description in ZONE_BINARY_SENSOR_TYPES:
if description.key in zone_data: if description.key in zone_data:
@ -145,6 +177,27 @@ class AirzoneAidooBinarySensor(AirzoneAidooEntity, AirzoneBinarySensor):
self._async_update_attrs() self._async_update_attrs()
class AirzoneSystemBinarySensor(AirzoneSystemEntity, AirzoneBinarySensor):
"""Define an Airzone Cloud System binary sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: AirzoneBinarySensorEntityDescription,
system_id: str,
system_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, system_id, system_data)
self._attr_unique_id = f"{system_id}_{description.key}"
self.entity_description = description
self._async_update_attrs()
class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor): class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor):
"""Define an Airzone Cloud Zone binary sensor.""" """Define an Airzone Cloud Zone binary sensor."""

View File

@ -1,13 +1,13 @@
"""The Airzone Cloud integration coordinator.""" """The Airzone Cloud integration coordinator."""
from __future__ import annotations from __future__ import annotations
from asyncio import timeout
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from aioairzone_cloud.cloudapi import AirzoneCloudApi from aioairzone_cloud.cloudapi import AirzoneCloudApi
from aioairzone_cloud.exceptions import AirzoneCloudError from aioairzone_cloud.exceptions import AirzoneCloudError
import async_timeout
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -35,7 +35,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC): async with timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
try: try:
await self.airzone.update() await self.airzone.update()
except AirzoneCloudError as error: except AirzoneCloudError as error:

View File

@ -10,13 +10,14 @@ from aioairzone_cloud.const import (
AZD_FIRMWARE, AZD_FIRMWARE,
AZD_NAME, AZD_NAME,
AZD_SYSTEM_ID, AZD_SYSTEM_ID,
AZD_SYSTEMS,
AZD_WEBSERVER, AZD_WEBSERVER,
AZD_WEBSERVERS, AZD_WEBSERVERS,
AZD_ZONES, AZD_ZONES,
) )
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER from .const import DOMAIN, MANUFACTURER
@ -65,6 +66,35 @@ class AirzoneAidooEntity(AirzoneEntity):
return value return value
class AirzoneSystemEntity(AirzoneEntity):
"""Define an Airzone Cloud System entity."""
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
system_id: str,
system_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator)
self.system_id = system_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, system_id)},
manufacturer=MANUFACTURER,
name=system_data[AZD_NAME],
via_device=(DOMAIN, system_data[AZD_WEBSERVER]),
)
def get_airzone_value(self, key: str) -> Any:
"""Return system value by key."""
value = None
if system := self.coordinator.data[AZD_SYSTEMS].get(self.system_id):
value = system.get(key)
return value
class AirzoneWebServerEntity(AirzoneEntity): class AirzoneWebServerEntity(AirzoneEntity):
"""Define an Airzone Cloud WebServer entity.""" """Define an Airzone Cloud WebServer entity."""

View File

@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES

View File

@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN

View File

@ -16,8 +16,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.event import async_call_later
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
CONF_DEVICE_BAUD, CONF_DEVICE_BAUD,
@ -66,9 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.async_add_executor_job(controller.open, baud) await hass.async_add_executor_job(controller.open, baud)
except NoDeviceError: except NoDeviceError:
_LOGGER.debug("Failed to connect. Retrying in 5 seconds") _LOGGER.debug("Failed to connect. Retrying in 5 seconds")
async_track_point_in_time( async_call_later(hass, timedelta(seconds=5), open_connection)
hass, open_connection, dt_util.utcnow() + timedelta(seconds=5)
)
return return
_LOGGER.debug("Established a connection with the alarmdecoder") _LOGGER.debug("Established a connection with the alarmdecoder")
hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True

View File

@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers import config_validation as cv, entityfilter
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import flash_briefings, intent, smart_home_http from . import flash_briefings, intent, smart_home
from .const import ( from .const import (
CONF_AUDIO, CONF_AUDIO,
CONF_DISPLAY_CATEGORIES, CONF_DISPLAY_CATEGORIES,
@ -100,6 +100,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if CONF_SMART_HOME in config: if CONF_SMART_HOME in config:
smart_home_config: dict[str, Any] | None = config[CONF_SMART_HOME] smart_home_config: dict[str, Any] | None = config[CONF_SMART_HOME]
smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) smart_home_config = smart_home_config or SMART_HOME_SCHEMA({})
await smart_home_http.async_setup(hass, smart_home_config) await smart_home.async_setup(hass, smart_home_config)
return True return True

View File

@ -1,15 +1,16 @@
"""Support for Alexa skill auth.""" """Support for Alexa skill auth."""
import asyncio import asyncio
from datetime import timedelta from asyncio import timeout
from datetime import datetime, timedelta
from http import HTTPStatus from http import HTTPStatus
import json import json
import logging import logging
from typing import Any
import aiohttp import aiohttp
import async_timeout
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -30,24 +31,24 @@ STORAGE_REFRESH_TOKEN = "refresh_token"
class Auth: class Auth:
"""Handle authentication to send events to Alexa.""" """Handle authentication to send events to Alexa."""
def __init__(self, hass, client_id, client_secret): def __init__(self, hass: HomeAssistant, client_id: str, client_secret: str) -> None:
"""Initialize the Auth class.""" """Initialize the Auth class."""
self.hass = hass self.hass = hass
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
self._prefs = None self._prefs: dict[str, Any] | None = None
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self._get_token_lock = asyncio.Lock() self._get_token_lock = asyncio.Lock()
async def async_do_auth(self, accept_grant_code): async def async_do_auth(self, accept_grant_code: str) -> str | None:
"""Do authentication with an AcceptGrant code.""" """Do authentication with an AcceptGrant code."""
# access token not retrieved yet for the first time, so this should # access token not retrieved yet for the first time, so this should
# be an access token request # be an access token request
lwa_params = { lwa_params: dict[str, str] = {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": accept_grant_code, "code": accept_grant_code,
CONF_CLIENT_ID: self.client_id, CONF_CLIENT_ID: self.client_id,
@ -61,25 +62,28 @@ class Auth:
return await self._async_request_new_token(lwa_params) return await self._async_request_new_token(lwa_params)
@callback @callback
def async_invalidate_access_token(self): def async_invalidate_access_token(self) -> None:
"""Invalidate access token.""" """Invalidate access token."""
assert self._prefs is not None
self._prefs[STORAGE_ACCESS_TOKEN] = None self._prefs[STORAGE_ACCESS_TOKEN] = None
async def async_get_access_token(self): async def async_get_access_token(self) -> str | None:
"""Perform access token or token refresh request.""" """Perform access token or token refresh request."""
async with self._get_token_lock: async with self._get_token_lock:
if self._prefs is None: if self._prefs is None:
await self.async_load_preferences() await self.async_load_preferences()
assert self._prefs is not None
if self.is_token_valid(): if self.is_token_valid():
_LOGGER.debug("Token still valid, using it") _LOGGER.debug("Token still valid, using it")
return self._prefs[STORAGE_ACCESS_TOKEN] token: str = self._prefs[STORAGE_ACCESS_TOKEN]
return token
if self._prefs[STORAGE_REFRESH_TOKEN] is None: if self._prefs[STORAGE_REFRESH_TOKEN] is None:
_LOGGER.debug("Token invalid and no refresh token available") _LOGGER.debug("Token invalid and no refresh token available")
return None return None
lwa_params = { lwa_params: dict[str, str] = {
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN], "refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
CONF_CLIENT_ID: self.client_id, CONF_CLIENT_ID: self.client_id,
@ -90,22 +94,26 @@ class Auth:
return await self._async_request_new_token(lwa_params) return await self._async_request_new_token(lwa_params)
@callback @callback
def is_token_valid(self): def is_token_valid(self) -> bool:
"""Check if a token is already loaded and if it is still valid.""" """Check if a token is already loaded and if it is still valid."""
assert self._prefs is not None
if not self._prefs[STORAGE_ACCESS_TOKEN]: if not self._prefs[STORAGE_ACCESS_TOKEN]:
return False return False
expire_time = dt_util.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME]) expire_time: datetime | None = dt_util.parse_datetime(
self._prefs[STORAGE_EXPIRE_TIME]
)
assert expire_time is not None
preemptive_expire_time = expire_time - timedelta( preemptive_expire_time = expire_time - timedelta(
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
) )
return dt_util.utcnow() < preemptive_expire_time return dt_util.utcnow() < preemptive_expire_time
async def _async_request_new_token(self, lwa_params): async def _async_request_new_token(self, lwa_params: dict[str, str]) -> str | None:
try: try:
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
async with async_timeout.timeout(10): async with timeout(10):
response = await session.post( response = await session.post(
LWA_TOKEN_URI, LWA_TOKEN_URI,
headers=LWA_HEADERS, headers=LWA_HEADERS,
@ -127,9 +135,9 @@ class Auth:
response_json = await response.json() response_json = await response.json()
_LOGGER.debug("LWA response body : %s", response_json) _LOGGER.debug("LWA response body : %s", response_json)
access_token = response_json["access_token"] access_token: str = response_json["access_token"]
refresh_token = response_json["refresh_token"] refresh_token: str = response_json["refresh_token"]
expires_in = response_json["expires_in"] expires_in: int = response_json["expires_in"]
expire_time = dt_util.utcnow() + timedelta(seconds=expires_in) expire_time = dt_util.utcnow() + timedelta(seconds=expires_in)
await self._async_update_preferences( await self._async_update_preferences(
@ -138,7 +146,7 @@ class Auth:
return access_token return access_token
async def async_load_preferences(self): async def async_load_preferences(self) -> None:
"""Load preferences with stored tokens.""" """Load preferences with stored tokens."""
self._prefs = await self._store.async_load() self._prefs = await self._store.async_load()
@ -149,10 +157,13 @@ class Auth:
STORAGE_EXPIRE_TIME: None, STORAGE_EXPIRE_TIME: None,
} }
async def _async_update_preferences(self, access_token, refresh_token, expire_time): async def _async_update_preferences(
self, access_token: str, refresh_token: str, expire_time: str
) -> None:
"""Update user preferences.""" """Update user preferences."""
if self._prefs is None: if self._prefs is None:
await self.async_load_preferences() await self.async_load_preferences()
assert self._prefs is not None
if access_token is not None: if access_token is not None:
self._prefs[STORAGE_ACCESS_TOKEN] = access_token self._prefs[STORAGE_ACCESS_TOKEN] = access_token

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import asyncio import asyncio
import logging import logging
from typing import Any
from yarl import URL
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
@ -33,38 +36,38 @@ class AbstractConfig(ABC):
await self._store.async_load() await self._store.async_load()
@property @property
def supports_auth(self): def supports_auth(self) -> bool:
"""Return if config supports auth.""" """Return if config supports auth."""
return False return False
@property @property
def should_report_state(self): def should_report_state(self) -> bool:
"""Return if states should be proactively reported.""" """Return if states should be proactively reported."""
return False return False
@property @property
def endpoint(self): @abstractmethod
def endpoint(self) -> str | URL | None:
"""Endpoint for report state.""" """Endpoint for report state."""
return None
@property @property
@abstractmethod @abstractmethod
def locale(self): def locale(self) -> str | None:
"""Return config locale.""" """Return config locale."""
@property @property
def entity_config(self): def entity_config(self) -> dict[str, Any]:
"""Return entity config.""" """Return entity config."""
return {} return {}
@property @property
def is_reporting_states(self): def is_reporting_states(self) -> bool:
"""Return if proactive mode is enabled.""" """Return if proactive mode is enabled."""
return self._unsub_proactive_report is not None return self._unsub_proactive_report is not None
@callback @callback
@abstractmethod @abstractmethod
def user_identifier(self): def user_identifier(self) -> str:
"""Return an identifier for the user that represents this config.""" """Return an identifier for the user that represents this config."""
async def async_enable_proactive_mode(self) -> None: async def async_enable_proactive_mode(self) -> None:
@ -85,29 +88,29 @@ class AbstractConfig(ABC):
self._unsub_proactive_report = None self._unsub_proactive_report = None
@callback @callback
def should_expose(self, entity_id): def should_expose(self, entity_id: str) -> bool:
"""If an entity should be exposed.""" """If an entity should be exposed."""
return False return False
@callback @callback
def async_invalidate_access_token(self): def async_invalidate_access_token(self) -> None:
"""Invalidate access token.""" """Invalidate access token."""
raise NotImplementedError raise NotImplementedError
async def async_get_access_token(self): async def async_get_access_token(self) -> str | None:
"""Get an access token.""" """Get an access token."""
raise NotImplementedError raise NotImplementedError
async def async_accept_grant(self, code): async def async_accept_grant(self, code: str) -> str | None:
"""Accept a grant.""" """Accept a grant."""
raise NotImplementedError raise NotImplementedError
@property @property
def authorized(self): def authorized(self) -> bool:
"""Return authorization status.""" """Return authorization status."""
return self._store.authorized return self._store.authorized
async def set_authorized(self, authorized) -> None: async def set_authorized(self, authorized: bool) -> None:
"""Set authorization status. """Set authorization status.
- Set when an incoming message is received from Alexa. - Set when an incoming message is received from Alexa.
@ -132,25 +135,26 @@ class AlexaConfigStore:
_STORAGE_VERSION = 1 _STORAGE_VERSION = 1
_STORAGE_KEY = DOMAIN _STORAGE_KEY = DOMAIN
def __init__(self, hass): def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a configuration store.""" """Initialize a configuration store."""
self._data = None self._data: dict[str, Any] | None = None
self._hass = hass self._hass = hass
self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) self._store: Store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY)
@property @property
def authorized(self): def authorized(self) -> bool:
"""Return authorization status.""" """Return authorization status."""
return self._data[STORE_AUTHORIZED] assert self._data is not None
return bool(self._data[STORE_AUTHORIZED])
@callback @callback
def set_authorized(self, authorized): def set_authorized(self, authorized: bool) -> None:
"""Set authorization status.""" """Set authorization status."""
if authorized != self._data[STORE_AUTHORIZED]: if self._data is not None and authorized != self._data[STORE_AUTHORIZED]:
self._data[STORE_AUTHORIZED] = authorized self._data[STORE_AUTHORIZED] = authorized
self._store.async_delay_save(lambda: self._data, 1.0) self._store.async_delay_save(lambda: self._data, 1.0)
async def async_load(self): async def async_load(self) -> None:
"""Load saved configuration from disk.""" """Load saved configuration from disk."""
if data := await self._store.async_load(): if data := await self._store.async_load():
self._data = data self._data = data

View File

@ -69,7 +69,7 @@ API_TEMP_UNITS = {
# Needs to be ordered dict for `async_api_set_thermostat_mode` which does a # Needs to be ordered dict for `async_api_set_thermostat_mode` which does a
# reverse mapping of this dict and we want to map the first occurrence of OFF # reverse mapping of this dict and we want to map the first occurrence of OFF
# back to HA state. # back to HA state.
API_THERMOSTAT_MODES = OrderedDict( API_THERMOSTAT_MODES: OrderedDict[str, str] = OrderedDict(
[ [
(climate.HVACMode.HEAT, "HEAT"), (climate.HVACMode.HEAT, "HEAT"),
(climate.HVACMode.COOL, "COOL"), (climate.HVACMode.COOL, "COOL"),

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Generator, Iterable from collections.abc import Generator, Iterable
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from homeassistant.components import ( from homeassistant.components import (
alarm_control_panel, alarm_control_panel,
@ -274,22 +274,23 @@ class AlexaEntity:
self.entity_conf = config.entity_config.get(entity.entity_id, {}) self.entity_conf = config.entity_config.get(entity.entity_id, {})
@property @property
def entity_id(self): def entity_id(self) -> str:
"""Return the Entity ID.""" """Return the Entity ID."""
return self.entity.entity_id return self.entity.entity_id
def friendly_name(self): def friendly_name(self) -> str:
"""Return the Alexa API friendly name.""" """Return the Alexa API friendly name."""
return self.entity_conf.get(CONF_NAME, self.entity.name).translate( friendly_name: str = self.entity_conf.get(
TRANSLATION_TABLE CONF_NAME, self.entity.name
) ).translate(TRANSLATION_TABLE)
return friendly_name
def description(self): def description(self) -> str:
"""Return the Alexa API description.""" """Return the Alexa API description."""
description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id
return f"{description} via Home Assistant".translate(TRANSLATION_TABLE) return f"{description} via Home Assistant".translate(TRANSLATION_TABLE)
def alexa_id(self): def alexa_id(self) -> str:
"""Return the Alexa API entity id.""" """Return the Alexa API entity id."""
return generate_alexa_id(self.entity.entity_id) return generate_alexa_id(self.entity.entity_id)
@ -317,7 +318,7 @@ class AlexaEntity:
""" """
raise NotImplementedError raise NotImplementedError
def serialize_properties(self): def serialize_properties(self) -> Generator[dict[str, Any], None, None]:
"""Yield each supported property in API format.""" """Yield each supported property in API format."""
for interface in self.interfaces(): for interface in self.interfaces():
if not interface.properties_proactively_reported(): if not interface.properties_proactively_reported():
@ -325,9 +326,9 @@ class AlexaEntity:
yield from interface.serialize_properties() yield from interface.serialize_properties()
def serialize_discovery(self): def serialize_discovery(self) -> dict[str, Any]:
"""Serialize the entity for discovery.""" """Serialize the entity for discovery."""
result = { result: dict[str, Any] = {
"displayCategories": self.display_categories(), "displayCategories": self.display_categories(),
"cookie": {}, "cookie": {},
"endpointId": self.alexa_id(), "endpointId": self.alexa_id(),
@ -366,7 +367,7 @@ def async_get_entities(
hass: HomeAssistant, config: AbstractConfig hass: HomeAssistant, config: AbstractConfig
) -> list[AlexaEntity]: ) -> list[AlexaEntity]:
"""Return all entities that are supported by Alexa.""" """Return all entities that are supported by Alexa."""
entities = [] entities: list[AlexaEntity] = []
for state in hass.states.async_all(): for state in hass.states.async_all():
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
continue continue
@ -725,7 +726,7 @@ class MediaPlayerCapabilities(AlexaEntity):
class SceneCapabilities(AlexaEntity): class SceneCapabilities(AlexaEntity):
"""Class to represent Scene capabilities.""" """Class to represent Scene capabilities."""
def description(self): def description(self) -> str:
"""Return the Alexa API description.""" """Return the Alexa API description."""
description = AlexaEntity.description(self) description = AlexaEntity.description(self)
if "scene" not in description.casefold(): if "scene" not in description.casefold():

View File

@ -1,8 +1,9 @@
"""Alexa related errors.""" """Alexa related errors."""
from __future__ import annotations from __future__ import annotations
from typing import Literal from typing import Any, Literal
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .const import API_TEMP_UNITS from .const import API_TEMP_UNITS
@ -29,7 +30,9 @@ class AlexaError(Exception):
namespace: str | None = None namespace: str | None = None
error_type: str | None = None error_type: str | None = None
def __init__(self, error_message, payload=None): def __init__(
self, error_message: str, payload: dict[str, Any] | None = None
) -> None:
"""Initialize an alexa error.""" """Initialize an alexa error."""
Exception.__init__(self) Exception.__init__(self)
self.error_message = error_message self.error_message = error_message
@ -42,7 +45,7 @@ class AlexaInvalidEndpointError(AlexaError):
namespace = "Alexa" namespace = "Alexa"
error_type = "NO_SUCH_ENDPOINT" error_type = "NO_SUCH_ENDPOINT"
def __init__(self, endpoint_id): def __init__(self, endpoint_id: str) -> None:
"""Initialize invalid endpoint error.""" """Initialize invalid endpoint error."""
msg = f"The endpoint {endpoint_id} does not exist" msg = f"The endpoint {endpoint_id} does not exist"
AlexaError.__init__(self, msg) AlexaError.__init__(self, msg)
@ -93,7 +96,9 @@ class AlexaTempRangeError(AlexaError):
namespace = "Alexa" namespace = "Alexa"
error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE" error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE"
def __init__(self, hass, temp, min_temp, max_temp): def __init__(
self, hass: HomeAssistant, temp: float, min_temp: float, max_temp: float
) -> None:
"""Initialize TempRange error.""" """Initialize TempRange error."""
unit = hass.config.units.temperature_unit unit = hass.config.units.temperature_unit
temp_range = { temp_range = {

View File

@ -4,10 +4,13 @@ from http import HTTPStatus
import logging import logging
import uuid import uuid
from aiohttp.web_response import StreamResponse
from homeassistant.components import http from homeassistant.components import http
from homeassistant.const import CONF_PASSWORD from homeassistant.const import CONF_PASSWORD
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.typing import ConfigType
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import ( from .const import (
@ -32,7 +35,7 @@ FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}"
@callback @callback
def async_setup(hass, flash_briefing_config): def async_setup(hass: HomeAssistant, flash_briefing_config: ConfigType) -> None:
"""Activate Alexa component.""" """Activate Alexa component."""
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config)) hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
@ -44,14 +47,16 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
requires_auth = False requires_auth = False
name = "api:alexa:flash_briefings" name = "api:alexa:flash_briefings"
def __init__(self, hass, flash_briefings): def __init__(self, hass: HomeAssistant, flash_briefings: ConfigType) -> None:
"""Initialize Alexa view.""" """Initialize Alexa view."""
super().__init__() super().__init__()
self.flash_briefings = flash_briefings self.flash_briefings = flash_briefings
template.attach(hass, self.flash_briefings) template.attach(hass, self.flash_briefings)
@callback @callback
def get(self, request, briefing_id): def get(
self, request: http.HomeAssistantRequest, briefing_id: str
) -> StreamResponse | tuple[bytes, HTTPStatus]:
"""Handle Alexa Flash Briefing request.""" """Handle Alexa Flash Briefing request."""
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) _LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)

View File

@ -75,8 +75,7 @@ from .errors import (
AlexaUnsupportedThermostatModeError, AlexaUnsupportedThermostatModeError,
AlexaVideoActionNotPermittedForContentError, AlexaVideoActionNotPermittedForContentError,
) )
from .messages import AlexaDirective, AlexaResponse from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_mode
from .state_report import async_enable_proactive_mode
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive" DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive"
@ -124,7 +123,7 @@ async def async_api_accept_grant(
Async friendly. Async friendly.
""" """
auth_code = directive.payload["grant"]["code"] auth_code: str = directive.payload["grant"]["code"]
_LOGGER.debug("AcceptGrant code: %s", auth_code) _LOGGER.debug("AcceptGrant code: %s", auth_code)
if config.supports_auth: if config.supports_auth:
@ -340,8 +339,8 @@ async def async_api_decrease_color_temp(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a decrease color temperature request.""" """Process a decrease color temperature request."""
entity = directive.entity entity = directive.entity
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes[light.ATTR_COLOR_TEMP])
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS)) max_mireds = int(entity.attributes[light.ATTR_MAX_MIREDS])
value = min(max_mireds, current + 50) value = min(max_mireds, current + 50)
await hass.services.async_call( await hass.services.async_call(
@ -364,8 +363,8 @@ async def async_api_increase_color_temp(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process an increase color temperature request.""" """Process an increase color temperature request."""
entity = directive.entity entity = directive.entity
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) current = int(entity.attributes[light.ATTR_COLOR_TEMP])
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS)) min_mireds = int(entity.attributes[light.ATTR_MIN_MIREDS])
value = max(min_mireds, current - 50) value = max(min_mireds, current - 50)
await hass.services.async_call( await hass.services.async_call(
@ -404,7 +403,7 @@ async def async_api_activate(
context=context, context=context,
) )
payload = { payload: dict[str, Any] = {
"cause": {"type": Cause.VOICE_INTERACTION}, "cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT), "timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
} }
@ -433,7 +432,7 @@ async def async_api_deactivate(
context=context, context=context,
) )
payload = { payload: dict[str, Any] = {
"cause": {"type": Cause.VOICE_INTERACTION}, "cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT), "timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
} }
@ -475,7 +474,24 @@ async def async_api_unlock(
context: ha.Context, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process an unlock request.""" """Process an unlock request."""
if config.locale not in {"de-DE", "en-US", "ja-JP"}: if config.locale not in {
"ar-SA",
"de-DE",
"en-AU",
"en-CA",
"en-GB",
"en-IN",
"en-US",
"es-ES",
"es-MX",
"es-US",
"fr-CA",
"fr-FR",
"hi-IN",
"it-IT",
"ja-JP",
"pt-BR",
}:
msg = ( msg = (
"The unlock directive is not supported for the following locales:" "The unlock directive is not supported for the following locales:"
f" {config.locale}" f" {config.locale}"
@ -510,7 +526,7 @@ async def async_api_set_volume(
volume = round(float(directive.payload["volume"] / 100), 2) volume = round(float(directive.payload["volume"] / 100), 2)
entity = directive.entity entity = directive.entity
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
} }
@ -555,7 +571,7 @@ async def async_api_select_input(
) )
raise AlexaInvalidValueError(msg) raise AlexaInvalidValueError(msg)
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_INPUT_SOURCE: media_input, media_player.const.ATTR_INPUT_SOURCE: media_input,
} }
@ -582,7 +598,7 @@ async def async_api_adjust_volume(
volume_delta = int(directive.payload["volume"]) volume_delta = int(directive.payload["volume"])
entity = directive.entity entity = directive.entity
current_level = entity.attributes.get(media_player.const.ATTR_MEDIA_VOLUME_LEVEL) current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL]
# read current state # read current state
try: try:
@ -592,7 +608,7 @@ async def async_api_adjust_volume(
volume = float(max(0, volume_delta + current) / 100) volume = float(max(0, volume_delta + current) / 100)
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume, media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
} }
@ -632,7 +648,7 @@ async def async_api_adjust_volume_step(
if is_default: if is_default:
volume_int = default_steps volume_int = default_steps
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
for _ in range(abs(volume_int)): for _ in range(abs(volume_int)):
await hass.services.async_call( await hass.services.async_call(
@ -653,7 +669,7 @@ async def async_api_set_mute(
"""Process a set mute request.""" """Process a set mute request."""
mute = bool(directive.payload["mute"]) mute = bool(directive.payload["mute"])
entity = directive.entity entity = directive.entity
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
} }
@ -674,7 +690,7 @@ async def async_api_play(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a play request.""" """Process a play request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context
@ -692,7 +708,7 @@ async def async_api_pause(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a pause request.""" """Process a pause request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context
@ -710,7 +726,7 @@ async def async_api_stop(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a stop request.""" """Process a stop request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context
@ -728,7 +744,7 @@ async def async_api_next(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a next request.""" """Process a next request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context
@ -746,7 +762,7 @@ async def async_api_previous(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a previous request.""" """Process a previous request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call( await hass.services.async_call(
entity.domain, entity.domain,
@ -759,7 +775,9 @@ async def async_api_previous(
return directive.response() return directive.response()
def temperature_from_object(hass, temp_obj, interval=False): def temperature_from_object(
hass: ha.HomeAssistant, temp_obj: dict[str, Any], interval: bool = False
) -> float:
"""Get temperature from Temperature object in requested unit.""" """Get temperature from Temperature object in requested unit."""
to_unit = hass.config.units.temperature_unit to_unit = hass.config.units.temperature_unit
from_unit = UnitOfTemperature.CELSIUS from_unit = UnitOfTemperature.CELSIUS
@ -785,11 +803,11 @@ async def async_api_set_target_temp(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a set target temperature request.""" """Process a set target temperature request."""
entity = directive.entity entity = directive.entity
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
unit = hass.config.units.temperature_unit unit = hass.config.units.temperature_unit
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
payload = directive.payload payload = directive.payload
response = directive.response() response = directive.response()
@ -849,9 +867,10 @@ async def async_api_adjust_target_temp(
context: ha.Context, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process an adjust target temperature request.""" """Process an adjust target temperature request."""
data: dict[str, Any]
entity = directive.entity entity = directive.entity
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
unit = hass.config.units.temperature_unit unit = hass.config.units.temperature_unit
temp_delta = temperature_from_object( temp_delta = temperature_from_object(
@ -862,7 +881,7 @@ async def async_api_adjust_target_temp(
current_target_temp_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) current_target_temp_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
if current_target_temp_high and current_target_temp_low: if current_target_temp_high is not None and current_target_temp_low is not None:
target_temp_high = float(current_target_temp_high) + temp_delta target_temp_high = float(current_target_temp_high) + temp_delta
if target_temp_high < min_temp or target_temp_high > max_temp: if target_temp_high < min_temp or target_temp_high > max_temp:
raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp) raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp)
@ -892,7 +911,7 @@ async def async_api_adjust_target_temp(
} }
) )
else: else:
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta target_temp = float(entity.attributes[ATTR_TEMPERATURE]) + temp_delta
if target_temp < min_temp or target_temp > max_temp: if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
@ -925,11 +944,13 @@ async def async_api_set_thermostat_mode(
context: ha.Context, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a set thermostat mode request.""" """Process a set thermostat mode request."""
operation_list: list[str]
entity = directive.entity entity = directive.entity
mode = directive.payload["thermostatMode"] mode = directive.payload["thermostatMode"]
mode = mode if isinstance(mode, str) else mode["value"] mode = mode if isinstance(mode, str) else mode["value"]
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None) ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None)
@ -944,7 +965,7 @@ async def async_api_set_thermostat_mode(
data[climate.ATTR_PRESET_MODE] = ha_preset data[climate.ATTR_PRESET_MODE] = ha_preset
elif mode == "CUSTOM": elif mode == "CUSTOM":
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, [])
custom_mode = directive.payload["thermostatMode"]["customName"] custom_mode = directive.payload["thermostatMode"]["customName"]
custom_mode = next( custom_mode = next(
(k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode),
@ -960,9 +981,13 @@ async def async_api_set_thermostat_mode(
data[climate.ATTR_HVAC_MODE] = custom_mode data[climate.ATTR_HVAC_MODE] = custom_mode
else: else:
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, [])
ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode} ha_modes: dict[str, str] = {
ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None) k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode
}
ha_mode: str | None = next(
iter(set(ha_modes).intersection(operation_list)), None
)
if ha_mode not in operation_list: if ha_mode not in operation_list:
msg = f"The requested thermostat mode {mode} is not supported" msg = f"The requested thermostat mode {mode} is not supported"
raise AlexaUnsupportedThermostatModeError(msg) raise AlexaUnsupportedThermostatModeError(msg)
@ -1007,7 +1032,7 @@ async def async_api_arm(
entity = directive.entity entity = directive.entity
service = None service = None
arm_state = directive.payload["armState"] arm_state = directive.payload["armState"]
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if entity.state != STATE_ALARM_DISARMED: if entity.state != STATE_ALARM_DISARMED:
msg = "You must disarm the system before you can set the requested arm state." msg = "You must disarm the system before you can set the requested arm state."
@ -1027,7 +1052,7 @@ async def async_api_arm(
) )
# return 0 until alarm integration supports an exit delay # return 0 until alarm integration supports an exit delay
payload = {"exitDelayInSeconds": 0} payload: dict[str, Any] = {"exitDelayInSeconds": 0}
response = directive.response( response = directive.response(
name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload
@ -1053,7 +1078,7 @@ async def async_api_disarm(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a Security Panel Disarm request.""" """Process a Security Panel Disarm request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
response = directive.response() response = directive.response()
# Per Alexa Documentation: If you receive a Disarm directive, and the # Per Alexa Documentation: If you receive a Disarm directive, and the
@ -1095,7 +1120,7 @@ async def async_api_set_mode(
instance = directive.instance instance = directive.instance
domain = entity.domain domain = entity.domain
service = None service = None
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
mode = directive.payload["mode"] mode = directive.payload["mode"]
# Fan Direction # Fan Direction
@ -1108,8 +1133,11 @@ async def async_api_set_mode(
# Fan preset_mode # Fan preset_mode
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
preset_mode = mode.split(".")[1] preset_mode = mode.split(".")[1]
if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get( preset_modes: list[str] | None = entity.attributes.get(fan.ATTR_PRESET_MODES)
fan.ATTR_PRESET_MODES if (
preset_mode != PRESET_MODE_NA
and preset_modes
and preset_mode in preset_modes
): ):
service = fan.SERVICE_SET_PRESET_MODE service = fan.SERVICE_SET_PRESET_MODE
data[fan.ATTR_PRESET_MODE] = preset_mode data[fan.ATTR_PRESET_MODE] = preset_mode
@ -1120,9 +1148,8 @@ async def async_api_set_mode(
# Humidifier mode # Humidifier mode
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
mode = mode.split(".")[1] mode = mode.split(".")[1]
if mode != PRESET_MODE_NA and mode in entity.attributes.get( modes: list[str] | None = entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES)
humidifier.ATTR_AVAILABLE_MODES if mode != PRESET_MODE_NA and modes and mode in modes:
):
service = humidifier.SERVICE_SET_MODE service = humidifier.SERVICE_SET_MODE
data[humidifier.ATTR_MODE] = mode data[humidifier.ATTR_MODE] = mode
else: else:
@ -1195,7 +1222,7 @@ async def async_api_toggle_on(
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
service = fan.SERVICE_OSCILLATE service = fan.SERVICE_OSCILLATE
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
fan.ATTR_OSCILLATING: True, fan.ATTR_OSCILLATING: True,
} }
@ -1234,7 +1261,7 @@ async def async_api_toggle_off(
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
service = fan.SERVICE_OSCILLATE service = fan.SERVICE_OSCILLATE
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
fan.ATTR_OSCILLATING: False, fan.ATTR_OSCILLATING: False,
} }
@ -1268,7 +1295,7 @@ async def async_api_set_range(
instance = directive.instance instance = directive.instance
domain = entity.domain domain = entity.domain
service = None service = None
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
range_value = directive.payload["rangeValue"] range_value = directive.payload["rangeValue"]
# Cover Position # Cover Position
@ -1537,7 +1564,7 @@ async def async_api_changechannel(
channel = metadata_payload["name"] channel = metadata_payload["name"]
payload_name = "callSign" payload_name = "callSign"
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_CONTENT_ID: channel, media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
media_player.const.ATTR_MEDIA_CONTENT_TYPE: ( media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
@ -1577,7 +1604,7 @@ async def async_api_skipchannel(
channel = int(directive.payload["channelCount"]) channel = int(directive.payload["channelCount"])
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if channel < 0: if channel < 0:
service_media = SERVICE_MEDIA_PREVIOUS_TRACK service_media = SERVICE_MEDIA_PREVIOUS_TRACK
@ -1624,7 +1651,7 @@ async def async_api_seek(
if media_duration and 0 < int(media_duration) < seek_position: if media_duration and 0 < int(media_duration) < seek_position:
seek_position = media_duration seek_position = media_duration
data = { data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_SEEK_POSITION: seek_position, media_player.ATTR_MEDIA_SEEK_POSITION: seek_position,
} }
@ -1640,7 +1667,9 @@ async def async_api_seek(
# convert seconds to milliseconds for StateReport. # convert seconds to milliseconds for StateReport.
seek_position = int(seek_position * 1000) seek_position = int(seek_position * 1000)
payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]} payload: dict[str, Any] = {
"properties": [{"name": "positionMilliseconds", "value": seek_position}]
}
return directive.response( return directive.response(
name="StateReport", namespace="Alexa.SeekController", payload=payload name="StateReport", namespace="Alexa.SeekController", payload=payload
) )
@ -1656,7 +1685,7 @@ async def async_api_set_eq_mode(
"""Process a SetMode request for EqualizerController.""" """Process a SetMode request for EqualizerController."""
mode = directive.payload["mode"] mode = directive.payload["mode"]
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
if sound_mode_list and mode.lower() in sound_mode_list: if sound_mode_list and mode.lower() in sound_mode_list:
@ -1702,7 +1731,7 @@ async def async_api_hold(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a TimeHoldController Hold request.""" """Process a TimeHoldController Hold request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == timer.DOMAIN: if entity.domain == timer.DOMAIN:
service = timer.SERVICE_PAUSE service = timer.SERVICE_PAUSE
@ -1729,7 +1758,7 @@ async def async_api_resume(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a TimeHoldController Resume request.""" """Process a TimeHoldController Resume request."""
entity = directive.entity entity = directive.entity
data = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == timer.DOMAIN: if entity.domain == timer.DOMAIN:
service = timer.SERVICE_START service = timer.SERVICE_START
@ -1774,7 +1803,7 @@ async def async_api_initialize_camera_stream(
"Failed to find suitable URL to serve to Alexa" "Failed to find suitable URL to serve to Alexa"
) from err ) from err
payload = { payload: dict[str, Any] = {
"cameraStreams": [ "cameraStreams": [
{ {
"uri": f"{external_url}{stream_source}", "uri": f"{external_url}{stream_source}",

View File

@ -3,8 +3,10 @@ import enum
import logging import logging
from typing import Any from typing import Any
from aiohttp.web import Response
from homeassistant.components import http from homeassistant.components import http
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
@ -18,7 +20,7 @@ HANDLERS = Registry() # type: ignore[var-annotated]
INTENTS_API_ENDPOINT = "/api/alexa" INTENTS_API_ENDPOINT = "/api/alexa"
class SpeechType(enum.Enum): class SpeechType(enum.StrEnum):
"""The Alexa speech types.""" """The Alexa speech types."""
plaintext = "PlainText" plaintext = "PlainText"
@ -28,7 +30,7 @@ class SpeechType(enum.Enum):
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml} SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
class CardType(enum.Enum): class CardType(enum.StrEnum):
"""The Alexa card types.""" """The Alexa card types."""
simple = "Simple" simple = "Simple"
@ -36,12 +38,12 @@ class CardType(enum.Enum):
@callback @callback
def async_setup(hass): def async_setup(hass: HomeAssistant) -> None:
"""Activate Alexa component.""" """Activate Alexa component."""
hass.http.register_view(AlexaIntentsView) hass.http.register_view(AlexaIntentsView)
async def async_setup_intents(hass): async def async_setup_intents(hass: HomeAssistant) -> None:
"""Do intents setup. """Do intents setup.
Right now this module does not expose any, but the intent component breaks Right now this module does not expose any, but the intent component breaks
@ -60,15 +62,15 @@ class AlexaIntentsView(http.HomeAssistantView):
url = INTENTS_API_ENDPOINT url = INTENTS_API_ENDPOINT
name = "api:alexa" name = "api:alexa"
async def post(self, request): async def post(self, request: http.HomeAssistantRequest) -> Response | bytes:
"""Handle Alexa.""" """Handle Alexa."""
hass = request.app["hass"] hass: HomeAssistant = request.app["hass"]
message = await request.json() message: dict[str, Any] = await request.json()
_LOGGER.debug("Received Alexa request: %s", message) _LOGGER.debug("Received Alexa request: %s", message)
try: try:
response = await async_handle_message(hass, message) response: dict[str, Any] = await async_handle_message(hass, message)
return b"" if response is None else self.json(response) return b"" if response is None else self.json(response)
except UnknownRequest as err: except UnknownRequest as err:
_LOGGER.warning(str(err)) _LOGGER.warning(str(err))
@ -99,15 +101,19 @@ class AlexaIntentsView(http.HomeAssistantView):
) )
def intent_error_response(hass, message, error): def intent_error_response(
hass: HomeAssistant, message: dict[str, Any], error: str
) -> dict[str, Any]:
"""Return an Alexa response that will speak the error message.""" """Return an Alexa response that will speak the error message."""
alexa_intent_info = message.get("request").get("intent") alexa_intent_info = message["request"].get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info) alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
alexa_response.add_speech(SpeechType.plaintext, error) alexa_response.add_speech(SpeechType.plaintext, error)
return alexa_response.as_dict() return alexa_response.as_dict()
async def async_handle_message(hass, message): async def async_handle_message(
hass: HomeAssistant, message: dict[str, Any]
) -> dict[str, Any]:
"""Handle an Alexa intent. """Handle an Alexa intent.
Raises: Raises:
@ -117,19 +123,22 @@ async def async_handle_message(hass, message):
- intent.IntentError - intent.IntentError
""" """
req = message.get("request") req = message["request"]
req_type = req["type"] req_type = req["type"]
if not (handler := HANDLERS.get(req_type)): if not (handler := HANDLERS.get(req_type)):
raise UnknownRequest(f"Received unknown request {req_type}") raise UnknownRequest(f"Received unknown request {req_type}")
return await handler(hass, message) response: dict[str, Any] = await handler(hass, message)
return response
@HANDLERS.register("SessionEndedRequest") @HANDLERS.register("SessionEndedRequest")
@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: HomeAssistant, message: dict[str, Any]
) -> dict[str, Any]:
"""Handle an intent request. """Handle an intent request.
Raises: Raises:
@ -138,9 +147,9 @@ async def async_handle_intent(hass, message):
- intent.IntentError - intent.IntentError
""" """
req = message.get("request") req = message["request"]
alexa_intent_info = req.get("intent") alexa_intent_info = req.get("intent")
alexa_response = AlexaResponse(hass, alexa_intent_info) alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
if req["type"] == "LaunchRequest": if req["type"] == "LaunchRequest":
intent_name = ( intent_name = (
@ -187,7 +196,7 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
# passes the id and name of the nearest possible slot resolution. For # passes the id and name of the nearest possible slot resolution. For
# reference to the request object structure, see the Alexa docs: # reference to the request object structure, see the Alexa docs:
# https://tinyurl.com/ybvm7jhs # https://tinyurl.com/ybvm7jhs
resolved_data = {} resolved_data: dict[str, Any] = {}
resolved_data["value"] = request["value"] resolved_data["value"] = request["value"]
resolved_data["id"] = "" resolved_data["id"] = ""
@ -226,18 +235,18 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
return resolved_data return resolved_data
class AlexaResponse: class AlexaIntentResponse:
"""Help generating the response for Alexa.""" """Help generating the response for Alexa."""
def __init__(self, hass, intent_info): def __init__(self, hass: HomeAssistant, intent_info: dict[str, Any] | None) -> None:
"""Initialize the response.""" """Initialize the response."""
self.hass = hass self.hass = hass
self.speech = None self.speech: dict[str, Any] | None = None
self.card = None self.card: dict[str, Any] | None = None
self.reprompt = None self.reprompt: dict[str, Any] | None = None
self.session_attributes = {} self.session_attributes: dict[str, Any] = {}
self.should_end_session = True self.should_end_session = True
self.variables = {} self.variables: dict[str, Any] = {}
# Intent is None if request was a LaunchRequest or SessionEndedRequest # Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None: if intent_info is not None:
@ -252,7 +261,7 @@ class AlexaResponse:
self.variables[_key] = _slot_data["value"] self.variables[_key] = _slot_data["value"]
self.variables[_key + "_Id"] = _slot_data["id"] self.variables[_key + "_Id"] = _slot_data["id"]
def add_card(self, card_type, title, content): def add_card(self, card_type: CardType, title: str, content: str) -> None:
"""Add a card to the response.""" """Add a card to the response."""
assert self.card is None assert self.card is None
@ -266,7 +275,7 @@ class AlexaResponse:
card["content"] = content card["content"] = content
self.card = card self.card = card
def add_speech(self, speech_type, text): def add_speech(self, speech_type: SpeechType, text: str) -> None:
"""Add speech to the response.""" """Add speech to the response."""
assert self.speech is None assert self.speech is None
@ -274,7 +283,7 @@ class AlexaResponse:
self.speech = {"type": speech_type.value, key: text} self.speech = {"type": speech_type.value, key: text}
def add_reprompt(self, speech_type, text): def add_reprompt(self, speech_type: SpeechType, text: str) -> None:
"""Add reprompt if user does not answer.""" """Add reprompt if user does not answer."""
assert self.reprompt is None assert self.reprompt is None
@ -284,9 +293,9 @@ class AlexaResponse:
self.reprompt = {"type": speech_type.value, key: text} self.reprompt = {"type": speech_type.value, key: text}
def as_dict(self): def as_dict(self) -> dict[str, Any]:
"""Return response in an Alexa valid dict.""" """Return response in an Alexa valid dict."""
response = {"shouldEndSession": self.should_end_session} response: dict[str, Any] = {"shouldEndSession": self.should_end_session}
if self.card is not None: if self.card is not None:
response["card"] = self.card response["card"] = self.card

View File

@ -1,20 +1,26 @@
"""Describe logbook events.""" """Describe logbook events."""
from collections.abc import Callable
from typing import Any
from homeassistant.components.logbook import ( from homeassistant.components.logbook import (
LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ENTITY_ID,
LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_MESSAGE,
LOGBOOK_ENTRY_NAME, LOGBOOK_ENTRY_NAME,
) )
from homeassistant.core import callback from homeassistant.core import Event, HomeAssistant, callback
from .const import DOMAIN, EVENT_ALEXA_SMART_HOME from .const import DOMAIN, EVENT_ALEXA_SMART_HOME
@callback @callback
def async_describe_events(hass, async_describe_event): def async_describe_events(
hass: HomeAssistant,
async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None],
) -> None:
"""Describe logbook events.""" """Describe logbook events."""
@callback @callback
def async_describe_logbook_event(event): def async_describe_logbook_event(event: Event) -> dict[str, Any]:
"""Describe a logbook event.""" """Describe a logbook event."""
data = event.data data = event.data

View File

@ -1,195 +0,0 @@
"""Alexa models."""
import logging
from uuid import uuid4
from .const import (
API_CONTEXT,
API_DIRECTIVE,
API_ENDPOINT,
API_EVENT,
API_HEADER,
API_PAYLOAD,
API_SCOPE,
)
from .entities import ENTITY_ADAPTERS
from .errors import AlexaInvalidEndpointError
_LOGGER = logging.getLogger(__name__)
class AlexaDirective:
"""An incoming Alexa directive."""
def __init__(self, request):
"""Initialize a directive."""
self._directive = request[API_DIRECTIVE]
self.namespace = self._directive[API_HEADER]["namespace"]
self.name = self._directive[API_HEADER]["name"]
self.payload = self._directive[API_PAYLOAD]
self.has_endpoint = API_ENDPOINT in self._directive
self.entity = self.entity_id = self.endpoint = self.instance = None
def load_entity(self, hass, config):
"""Set attributes related to the entity for this request.
Sets these attributes when self.has_endpoint is True:
- entity
- entity_id
- endpoint
- instance (when header includes instance property)
Behavior when self.has_endpoint is False is undefined.
Will raise AlexaInvalidEndpointError if the endpoint in the request is
malformed or nonexistent.
"""
_endpoint_id = self._directive[API_ENDPOINT]["endpointId"]
self.entity_id = _endpoint_id.replace("#", ".")
self.entity = hass.states.get(self.entity_id)
if not self.entity or not config.should_expose(self.entity_id):
raise AlexaInvalidEndpointError(_endpoint_id)
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)
if "instance" in self._directive[API_HEADER]:
self.instance = self._directive[API_HEADER]["instance"]
def response(self, name="Response", namespace="Alexa", payload=None):
"""Create an API formatted response.
Async friendly.
"""
response = AlexaResponse(name, namespace, payload)
token = self._directive[API_HEADER].get("correlationToken")
if token:
response.set_correlation_token(token)
if self.has_endpoint:
response.set_endpoint(self._directive[API_ENDPOINT].copy())
return response
def error(
self,
namespace="Alexa",
error_type="INTERNAL_ERROR",
error_message="",
payload=None,
):
"""Create a API formatted error response.
Async friendly.
"""
payload = payload or {}
payload["type"] = error_type
payload["message"] = error_message
_LOGGER.info(
"Request %s/%s error %s: %s",
self._directive[API_HEADER]["namespace"],
self._directive[API_HEADER]["name"],
error_type,
error_message,
)
return self.response(name="ErrorResponse", namespace=namespace, payload=payload)
class AlexaResponse:
"""Class to hold a response."""
def __init__(self, name, namespace, payload=None):
"""Initialize the response."""
payload = payload or {}
self._response = {
API_EVENT: {
API_HEADER: {
"namespace": namespace,
"name": name,
"messageId": str(uuid4()),
"payloadVersion": "3",
},
API_PAYLOAD: payload,
}
}
@property
def name(self):
"""Return the name of this response."""
return self._response[API_EVENT][API_HEADER]["name"]
@property
def namespace(self):
"""Return the namespace of this response."""
return self._response[API_EVENT][API_HEADER]["namespace"]
def set_correlation_token(self, token):
"""Set the correlationToken.
This should normally mirror the value from a request, and is set by
AlexaDirective.response() usually.
"""
self._response[API_EVENT][API_HEADER]["correlationToken"] = token
def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None):
"""Set the endpoint dictionary.
This is used to send proactive messages to Alexa.
"""
self._response[API_EVENT][API_ENDPOINT] = {
API_SCOPE: {"type": "BearerToken", "token": bearer_token}
}
if endpoint_id is not None:
self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id
if cookie is not None:
self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie
def set_endpoint(self, endpoint):
"""Set the endpoint.
This should normally mirror the value from a request, and is set by
AlexaDirective.response() usually.
"""
self._response[API_EVENT][API_ENDPOINT] = endpoint
def _properties(self):
context = self._response.setdefault(API_CONTEXT, {})
return context.setdefault("properties", [])
def add_context_property(self, prop):
"""Add a property to the response context.
The Alexa response includes a list of properties which provides
feedback on how states have changed. For example if a user asks,
"Alexa, set thermostat to 20 degrees", the API expects a response with
the new value of the property, and Alexa will respond to the user
"Thermostat set to 20 degrees".
async_handle_message() will call .merge_context_properties() for every
request automatically, however often handlers will call services to
change state but the effects of those changes are applied
asynchronously. Thus, handlers should call this method to confirm
changes before returning.
"""
self._properties().append(prop)
def merge_context_properties(self, endpoint):
"""Add all properties from given endpoint if not already set.
Handlers should be using .add_context_property().
"""
properties = self._properties()
already_set = {(p["namespace"], p["name"]) for p in properties}
for prop in endpoint.serialize_properties():
if (prop["namespace"], prop["name"]) not in already_set:
self.add_context_property(prop)
def serialize(self):
"""Return response as a JSON-able data structure."""
return self._response

View File

@ -1,6 +1,9 @@
"""Alexa Resources and Assets.""" """Alexa Resources and Assets."""
from typing import Any
class AlexaGlobalCatalog: class AlexaGlobalCatalog:
"""The Global Alexa catalog. """The Global Alexa catalog.
@ -207,36 +210,40 @@ class AlexaCapabilityResource:
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
""" """
def __init__(self, labels): def __init__(self, labels: list[str]) -> None:
"""Initialize an Alexa resource.""" """Initialize an Alexa resource."""
self._resource_labels = [] self._resource_labels = []
for label in labels: for label in labels:
self._resource_labels.append(label) self._resource_labels.append(label)
def serialize_capability_resources(self): def serialize_capability_resources(self) -> dict[str, list[dict[str, Any]]]:
"""Return capabilityResources object serialized for an API response.""" """Return capabilityResources object serialized for an API response."""
return self.serialize_labels(self._resource_labels) return self.serialize_labels(self._resource_labels)
def serialize_configuration(self): def serialize_configuration(self) -> dict[str, Any]:
"""Return serialized configuration for an API response. """Return serialized configuration for an API response.
Return ModeResources, PresetResources friendlyNames serialized. Return ModeResources, PresetResources friendlyNames serialized.
""" """
return [] raise NotImplementedError()
def serialize_labels(self, resources): def serialize_labels(self, resources: list[str]) -> dict[str, list[dict[str, Any]]]:
"""Return serialized labels for an API response. """Return serialized labels for an API response.
Returns resource label objects for friendlyNames serialized. Returns resource label objects for friendlyNames serialized.
""" """
labels = [] labels: list[dict[str, Any]] = []
label_dict: dict[str, Any]
for label in resources: for label in resources:
if label in AlexaGlobalCatalog.__dict__.values(): if label in AlexaGlobalCatalog.__dict__.values():
label = {"@type": "asset", "value": {"assetId": label}} label_dict = {"@type": "asset", "value": {"assetId": label}}
else: else:
label = {"@type": "text", "value": {"text": label, "locale": "en-US"}} label_dict = {
"@type": "text",
"value": {"text": label, "locale": "en-US"},
}
labels.append(label) labels.append(label_dict)
return {"friendlyNames": labels} return {"friendlyNames": labels}
@ -247,22 +254,22 @@ class AlexaModeResource(AlexaCapabilityResource):
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
""" """
def __init__(self, labels, ordered=False): def __init__(self, labels: list[str], ordered: bool = False) -> None:
"""Initialize an Alexa modeResource.""" """Initialize an Alexa modeResource."""
super().__init__(labels) super().__init__(labels)
self._supported_modes = [] self._supported_modes: list[dict[str, Any]] = []
self._mode_ordered = ordered self._mode_ordered: bool = ordered
def add_mode(self, value, labels): def add_mode(self, value: str, labels: list[str]) -> None:
"""Add mode to the supportedModes object.""" """Add mode to the supportedModes object."""
self._supported_modes.append({"value": value, "labels": labels}) self._supported_modes.append({"value": value, "labels": labels})
def serialize_configuration(self): def serialize_configuration(self) -> dict[str, Any]:
"""Return serialized configuration for an API response. """Return serialized configuration for an API response.
Returns configuration for ModeResources friendlyNames serialized. Returns configuration for ModeResources friendlyNames serialized.
""" """
mode_resources = [] mode_resources: list[dict[str, Any]] = []
for mode in self._supported_modes: for mode in self._supported_modes:
result = { result = {
"value": mode["value"], "value": mode["value"],
@ -282,10 +289,17 @@ class AlexaPresetResource(AlexaCapabilityResource):
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources
""" """
def __init__(self, labels, min_value, max_value, precision, unit=None): def __init__(
self,
labels: list[str],
min_value: int | float,
max_value: int | float,
precision: int | float,
unit: str | None = None,
) -> None:
"""Initialize an Alexa presetResource.""" """Initialize an Alexa presetResource."""
super().__init__(labels) super().__init__(labels)
self._presets = [] self._presets: list[dict[str, Any]] = []
self._minimum_value = min_value self._minimum_value = min_value
self._maximum_value = max_value self._maximum_value = max_value
self._precision = precision self._precision = precision
@ -293,16 +307,16 @@ class AlexaPresetResource(AlexaCapabilityResource):
if unit in AlexaGlobalCatalog.__dict__.values(): if unit in AlexaGlobalCatalog.__dict__.values():
self._unit_of_measure = unit self._unit_of_measure = unit
def add_preset(self, value, labels): def add_preset(self, value: int | float, labels: list[str]) -> None:
"""Add preset to configuration presets array.""" """Add preset to configuration presets array."""
self._presets.append({"value": value, "labels": labels}) self._presets.append({"value": value, "labels": labels})
def serialize_configuration(self): def serialize_configuration(self) -> dict[str, Any]:
"""Return serialized configuration for an API response. """Return serialized configuration for an API response.
Returns configuration for PresetResources friendlyNames serialized. Returns configuration for PresetResources friendlyNames serialized.
""" """
configuration = { configuration: dict[str, Any] = {
"supportedRange": { "supportedRange": {
"minimumValue": self._minimum_value, "minimumValue": self._minimum_value,
"maximumValue": self._maximum_value, "maximumValue": self._maximum_value,
@ -372,26 +386,28 @@ class AlexaSemantics:
DIRECTIVE_MODE_SET_MODE = "SetMode" DIRECTIVE_MODE_SET_MODE = "SetMode"
DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode" DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode"
def __init__(self): def __init__(self) -> None:
"""Initialize an Alexa modeResource.""" """Initialize an Alexa modeResource."""
self._action_mappings = [] self._action_mappings: list[dict[str, Any]] = []
self._state_mappings = [] self._state_mappings: list[dict[str, Any]] = []
def _add_action_mapping(self, semantics): def _add_action_mapping(self, semantics: dict[str, Any]) -> None:
"""Add action mapping between actions and interface directives.""" """Add action mapping between actions and interface directives."""
self._action_mappings.append(semantics) self._action_mappings.append(semantics)
def _add_state_mapping(self, semantics): def _add_state_mapping(self, semantics: dict[str, Any]) -> None:
"""Add state mapping between states and interface directives.""" """Add state mapping between states and interface directives."""
self._state_mappings.append(semantics) self._state_mappings.append(semantics)
def add_states_to_value(self, states, value): def add_states_to_value(self, states: list[str], value: Any) -> None:
"""Add StatesToValue stateMappings.""" """Add StatesToValue stateMappings."""
self._add_state_mapping( self._add_state_mapping(
{"@type": self.STATES_TO_VALUE, "states": states, "value": value} {"@type": self.STATES_TO_VALUE, "states": states, "value": value}
) )
def add_states_to_range(self, states, min_value, max_value): def add_states_to_range(
self, states: list[str], min_value: int | float, max_value: int | float
) -> None:
"""Add StatesToRange stateMappings.""" """Add StatesToRange stateMappings."""
self._add_state_mapping( self._add_state_mapping(
{ {
@ -401,7 +417,9 @@ class AlexaSemantics:
} }
) )
def add_action_to_directive(self, actions, directive, payload): def add_action_to_directive(
self, actions: list[str], directive: str, payload: dict[str, Any]
) -> None:
"""Add ActionsToDirective actionMappings.""" """Add ActionsToDirective actionMappings."""
self._add_action_mapping( self._add_action_mapping(
{ {
@ -411,9 +429,9 @@ class AlexaSemantics:
} }
) )
def serialize_semantics(self): def serialize_semantics(self) -> dict[str, Any]:
"""Return semantics object serialized for an API response.""" """Return semantics object serialized for an API response."""
semantics = {} semantics: dict[str, Any] = {}
if self._action_mappings: if self._action_mappings:
semantics[self.MAPPINGS_ACTION] = self._action_mappings semantics[self.MAPPINGS_ACTION] = self._action_mappings
if self._state_mappings: if self._state_mappings:

View File

@ -1,17 +1,170 @@
"""Support for alexa Smart Home Skill API.""" """Support for alexa Smart Home Skill API."""
import logging import logging
from typing import Any
import homeassistant.core as ha from aiohttp import web
from yarl import URL
from .const import API_DIRECTIVE, API_HEADER, EVENT_ALEXA_SMART_HOME from homeassistant import core
from homeassistant.auth.models import User
from homeassistant.components.http import HomeAssistantRequest
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import ConfigType
from .auth import Auth
from .config import AbstractConfig
from .const import (
API_DIRECTIVE,
API_HEADER,
CONF_ENDPOINT,
CONF_ENTITY_CONFIG,
CONF_FILTER,
CONF_LOCALE,
EVENT_ALEXA_SMART_HOME,
)
from .errors import AlexaBridgeUnreachableError, AlexaError from .errors import AlexaBridgeUnreachableError, AlexaError
from .handlers import HANDLERS from .handlers import HANDLERS
from .messages import AlexaDirective from .state_report import AlexaDirective
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
async def async_handle_message(hass, config, request, context=None, enabled=True): class AlexaConfig(AbstractConfig):
"""Alexa config."""
_auth: Auth | None
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
"""Initialize Alexa config."""
super().__init__(hass)
self._config = config
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET])
else:
self._auth = None
@property
def supports_auth(self) -> bool:
"""Return if config supports auth."""
return self._auth is not None
@property
def should_report_state(self) -> bool:
"""Return if we should proactively report states."""
return self._auth is not None and self.authorized
@property
def endpoint(self) -> str | URL | None:
"""Endpoint for report state."""
return self._config.get(CONF_ENDPOINT)
@property
def entity_config(self) -> dict[str, Any]:
"""Return entity config."""
return self._config.get(CONF_ENTITY_CONFIG) or {}
@property
def locale(self) -> str | None:
"""Return config locale."""
return self._config.get(CONF_LOCALE)
@core.callback
def user_identifier(self) -> str:
"""Return an identifier for the user that represents this config."""
return ""
@core.callback
def should_expose(self, entity_id: str) -> bool:
"""If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter:
return bool(self._config[CONF_FILTER](entity_id))
entity_registry = er.async_get(self.hass)
if registry_entry := entity_registry.async_get(entity_id):
auxiliary_entity = (
registry_entry.entity_category is not None
or registry_entry.hidden_by is not None
)
else:
auxiliary_entity = False
return not auxiliary_entity
@core.callback
def async_invalidate_access_token(self) -> None:
"""Invalidate access token."""
assert self._auth is not None
self._auth.async_invalidate_access_token()
async def async_get_access_token(self) -> str | None:
"""Get an access token."""
assert self._auth is not None
return await self._auth.async_get_access_token()
async def async_accept_grant(self, code: str) -> str | None:
"""Accept a grant."""
assert self._auth is not None
return await self._auth.async_do_auth(code)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> None:
"""Activate Smart Home functionality of Alexa component.
This is optional, triggered by having a `smart_home:` sub-section in the
alexa configuration.
Even if that's disabled, the functionality in this module may still be used
by the cloud component which will call async_handle_message directly.
"""
smart_home_config = AlexaConfig(hass, config)
await smart_home_config.async_initialize()
hass.http.register_view(SmartHomeView(smart_home_config))
if smart_home_config.should_report_state:
await smart_home_config.async_enable_proactive_mode()
class SmartHomeView(HomeAssistantView):
"""Expose Smart Home v3 payload interface via HTTP POST."""
url = SMART_HOME_HTTP_ENDPOINT
name = "api:alexa:smart_home"
def __init__(self, smart_home_config: AlexaConfig) -> None:
"""Initialize."""
self.smart_home_config = smart_home_config
async def post(self, request: HomeAssistantRequest) -> web.Response | bytes:
"""Handle Alexa Smart Home requests.
The Smart Home API requires the endpoint to be implemented in AWS
Lambda, which will need to forward the requests to here and pass back
the response.
"""
hass: HomeAssistant = request.app["hass"]
user: User = request["hass_user"]
message: dict[str, Any] = await request.json()
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
response = await async_handle_message(
hass, self.smart_home_config, message, context=core.Context(user_id=user.id)
)
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
return b"" if response is None else self.json(response)
async def async_handle_message(
hass: HomeAssistant,
config: AbstractConfig,
request: dict[str, Any],
context: Context | None = None,
enabled: bool = True,
) -> dict[str, Any]:
"""Handle incoming API messages. """Handle incoming API messages.
If enabled is False, the response to all messages will be a If enabled is False, the response to all messages will be a
@ -21,7 +174,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3" assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3"
if context is None: if context is None:
context = ha.Context() context = Context()
directive = AlexaDirective(request) directive = AlexaDirective(request)
@ -48,7 +201,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
response = directive.error() response = directive.error()
except AlexaError as err: except AlexaError as err:
response = directive.error( response = directive.error(
error_type=err.error_type, error_type=str(err.error_type),
error_message=err.error_message, error_message=err.error_message,
payload=err.payload, payload=err.payload,
) )
@ -61,9 +214,13 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
) )
response = directive.error(error_message="Unknown error") response = directive.error(error_message="Unknown error")
request_info = {"namespace": directive.namespace, "name": directive.name} request_info: dict[str, Any] = {
"namespace": directive.namespace,
"name": directive.name,
}
if directive.has_endpoint: if directive.has_endpoint:
assert directive.entity_id is not None
request_info["entity_id"] = directive.entity_id request_info["entity_id"] = directive.entity_id
hass.bus.async_fire( hass.bus.async_fire(

View File

@ -1,137 +0,0 @@
"""Alexa HTTP interface."""
import logging
from homeassistant import core
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import ConfigType
from .auth import Auth
from .config import AbstractConfig
from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE
from .smart_home import async_handle_message
_LOGGER = logging.getLogger(__name__)
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
class AlexaConfig(AbstractConfig):
"""Alexa config."""
def __init__(self, hass, config):
"""Initialize Alexa config."""
super().__init__(hass)
self._config = config
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET])
else:
self._auth = None
@property
def supports_auth(self):
"""Return if config supports auth."""
return self._auth is not None
@property
def should_report_state(self):
"""Return if we should proactively report states."""
return self._auth is not None and self.authorized
@property
def endpoint(self):
"""Endpoint for report state."""
return self._config.get(CONF_ENDPOINT)
@property
def entity_config(self):
"""Return entity config."""
return self._config.get(CONF_ENTITY_CONFIG) or {}
@property
def locale(self):
"""Return config locale."""
return self._config.get(CONF_LOCALE)
@core.callback
def user_identifier(self):
"""Return an identifier for the user that represents this config."""
return ""
@core.callback
def should_expose(self, entity_id):
"""If an entity should be exposed."""
if not self._config[CONF_FILTER].empty_filter:
return self._config[CONF_FILTER](entity_id)
entity_registry = er.async_get(self.hass)
if registry_entry := entity_registry.async_get(entity_id):
auxiliary_entity = (
registry_entry.entity_category is not None
or registry_entry.hidden_by is not None
)
else:
auxiliary_entity = False
return not auxiliary_entity
@core.callback
def async_invalidate_access_token(self):
"""Invalidate access token."""
self._auth.async_invalidate_access_token()
async def async_get_access_token(self):
"""Get an access token."""
return await self._auth.async_get_access_token()
async def async_accept_grant(self, code):
"""Accept a grant."""
return await self._auth.async_do_auth(code)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> None:
"""Activate Smart Home functionality of Alexa component.
This is optional, triggered by having a `smart_home:` sub-section in the
alexa configuration.
Even if that's disabled, the functionality in this module may still be used
by the cloud component which will call async_handle_message directly.
"""
smart_home_config = AlexaConfig(hass, config)
await smart_home_config.async_initialize()
hass.http.register_view(SmartHomeView(smart_home_config))
if smart_home_config.should_report_state:
await smart_home_config.async_enable_proactive_mode()
class SmartHomeView(HomeAssistantView):
"""Expose Smart Home v3 payload interface via HTTP POST."""
url = SMART_HOME_HTTP_ENDPOINT
name = "api:alexa:smart_home"
def __init__(self, smart_home_config):
"""Initialize."""
self.smart_home_config = smart_home_config
async def post(self, request):
"""Handle Alexa Smart Home requests.
The Smart Home API requires the endpoint to be implemented in AWS
Lambda, which will need to forward the requests to here and pass back
the response.
"""
hass = request.app["hass"]
user = request["hass_user"]
message = await request.json()
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
response = await async_handle_message(
hass, self.smart_home_config, message, context=core.Context(user_id=user.id)
)
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
return b"" if response is None else self.json(response)

View File

@ -2,27 +2,40 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from http import HTTPStatus from http import HTTPStatus
import json import json
import logging import logging
from typing import TYPE_CHECKING, cast from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast
from uuid import uuid4
import aiohttp import aiohttp
import async_timeout
from homeassistant.components import event from homeassistant.components import event
from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.const import MATCH_ALL, STATE_ON
from homeassistant.core import HomeAssistant, State, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.significant_change import create_checker from homeassistant.helpers.significant_change import create_checker
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.util.json import JsonObjectType, json_loads_object from homeassistant.util.json import JsonObjectType, json_loads_object
from .const import API_CHANGE, DATE_FORMAT, DOMAIN, Cause from .const import (
API_CHANGE,
API_CONTEXT,
API_DIRECTIVE,
API_ENDPOINT,
API_EVENT,
API_HEADER,
API_PAYLOAD,
API_SCOPE,
DATE_FORMAT,
DOMAIN,
Cause,
)
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
from .errors import NoTokenAvailable, RequireRelink from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink
from .messages import AlexaResponse
if TYPE_CHECKING: if TYPE_CHECKING:
from .config import AbstractConfig from .config import AbstractConfig
@ -31,7 +44,202 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
async def async_enable_proactive_mode(hass, smart_home_config): class AlexaDirective:
"""An incoming Alexa directive."""
entity: State
entity_id: str | None
endpoint: AlexaEntity
instance: str | None
def __init__(self, request: dict[str, Any]) -> None:
"""Initialize a directive."""
self._directive: dict[str, Any] = request[API_DIRECTIVE]
self.namespace: str = self._directive[API_HEADER]["namespace"]
self.name: str = self._directive[API_HEADER]["name"]
self.payload: dict[str, Any] = self._directive[API_PAYLOAD]
self.has_endpoint: bool = API_ENDPOINT in self._directive
self.instance = None
self.entity_id = None
def load_entity(self, hass: HomeAssistant, config: AbstractConfig) -> None:
"""Set attributes related to the entity for this request.
Sets these attributes when self.has_endpoint is True:
- entity
- entity_id
- endpoint
- instance (when header includes instance property)
Behavior when self.has_endpoint is False is undefined.
Will raise AlexaInvalidEndpointError if the endpoint in the request is
malformed or nonexistent.
"""
_endpoint_id: str = self._directive[API_ENDPOINT]["endpointId"]
self.entity_id = _endpoint_id.replace("#", ".")
entity: State | None = hass.states.get(self.entity_id)
if not entity or not config.should_expose(self.entity_id):
raise AlexaInvalidEndpointError(_endpoint_id)
self.entity = entity
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)
if "instance" in self._directive[API_HEADER]:
self.instance = self._directive[API_HEADER]["instance"]
def response(
self,
name: str = "Response",
namespace: str = "Alexa",
payload: dict[str, Any] | None = None,
) -> AlexaResponse:
"""Create an API formatted response.
Async friendly.
"""
response = AlexaResponse(name, namespace, payload)
token = self._directive[API_HEADER].get("correlationToken")
if token:
response.set_correlation_token(token)
if self.has_endpoint:
response.set_endpoint(self._directive[API_ENDPOINT].copy())
return response
def error(
self,
namespace: str = "Alexa",
error_type: str = "INTERNAL_ERROR",
error_message: str = "",
payload: dict[str, Any] | None = None,
) -> AlexaResponse:
"""Create a API formatted error response.
Async friendly.
"""
payload = payload or {}
payload["type"] = error_type
payload["message"] = error_message
_LOGGER.info(
"Request %s/%s error %s: %s",
self._directive[API_HEADER]["namespace"],
self._directive[API_HEADER]["name"],
error_type,
error_message,
)
return self.response(name="ErrorResponse", namespace=namespace, payload=payload)
class AlexaResponse:
"""Class to hold a response."""
def __init__(
self, name: str, namespace: str, payload: dict[str, Any] | None = None
) -> None:
"""Initialize the response."""
payload = payload or {}
self._response: dict[str, Any] = {
API_EVENT: {
API_HEADER: {
"namespace": namespace,
"name": name,
"messageId": str(uuid4()),
"payloadVersion": "3",
},
API_PAYLOAD: payload,
}
}
@property
def name(self) -> str:
"""Return the name of this response."""
name: str = self._response[API_EVENT][API_HEADER]["name"]
return name
@property
def namespace(self) -> str:
"""Return the namespace of this response."""
namespace: str = self._response[API_EVENT][API_HEADER]["namespace"]
return namespace
def set_correlation_token(self, token: str) -> None:
"""Set the correlationToken.
This should normally mirror the value from a request, and is set by
AlexaDirective.response() usually.
"""
self._response[API_EVENT][API_HEADER]["correlationToken"] = token
def set_endpoint_full(
self, bearer_token: str | None, endpoint_id: str | None
) -> None:
"""Set the endpoint dictionary.
This is used to send proactive messages to Alexa.
"""
self._response[API_EVENT][API_ENDPOINT] = {
API_SCOPE: {"type": "BearerToken", "token": bearer_token}
}
if endpoint_id is not None:
self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id
def set_endpoint(self, endpoint: dict[str, Any]) -> None:
"""Set the endpoint.
This should normally mirror the value from a request, and is set by
AlexaDirective.response() usually.
"""
self._response[API_EVENT][API_ENDPOINT] = endpoint
def _properties(self) -> list[dict[str, Any]]:
context: dict[str, Any] = self._response.setdefault(API_CONTEXT, {})
properties: list[dict[str, Any]] = context.setdefault("properties", [])
return properties
def add_context_property(self, prop: dict[str, Any]) -> None:
"""Add a property to the response context.
The Alexa response includes a list of properties which provides
feedback on how states have changed. For example if a user asks,
"Alexa, set thermostat to 20 degrees", the API expects a response with
the new value of the property, and Alexa will respond to the user
"Thermostat set to 20 degrees".
async_handle_message() will call .merge_context_properties() for every
request automatically, however often handlers will call services to
change state but the effects of those changes are applied
asynchronously. Thus, handlers should call this method to confirm
changes before returning.
"""
self._properties().append(prop)
def merge_context_properties(self, endpoint: AlexaEntity) -> None:
"""Add all properties from given endpoint if not already set.
Handlers should be using .add_context_property().
"""
properties = self._properties()
already_set = {(p["namespace"], p["name"]) for p in properties}
for prop in endpoint.serialize_properties():
if (prop["namespace"], prop["name"]) not in already_set:
self.add_context_property(prop)
def serialize(self) -> dict[str, Any]:
"""Return response as a JSON-able data structure."""
return self._response
async def async_enable_proactive_mode(
hass: HomeAssistant, smart_home_config: AbstractConfig
) -> CALLBACK_TYPE | None:
"""Enable the proactive mode. """Enable the proactive mode.
Proactive mode makes this component report state changes to Alexa. Proactive mode makes this component report state changes to Alexa.
@ -43,12 +251,12 @@ async def async_enable_proactive_mode(hass, smart_home_config):
def extra_significant_check( def extra_significant_check(
hass: HomeAssistant, hass: HomeAssistant,
old_state: str, old_state: str,
old_attrs: dict, old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
old_extra_arg: dict, old_extra_arg: Any,
new_state: str, new_state: str,
new_attrs: dict, new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
new_extra_arg: dict, new_extra_arg: Any,
): ) -> bool:
"""Check if the serialized data has changed.""" """Check if the serialized data has changed."""
return old_extra_arg is not None and old_extra_arg != new_extra_arg return old_extra_arg is not None and old_extra_arg != new_extra_arg
@ -58,7 +266,7 @@ async def async_enable_proactive_mode(hass, smart_home_config):
changed_entity: str, changed_entity: str,
old_state: State | None, old_state: State | None,
new_state: State | None, new_state: State | None,
): ) -> None:
if not hass.is_running: if not hass.is_running:
return return
@ -117,8 +325,13 @@ async def async_enable_proactive_mode(hass, smart_home_config):
async def async_send_changereport_message( async def async_send_changereport_message(
hass, config, alexa_entity, alexa_properties, *, invalidate_access_token=True hass: HomeAssistant,
): config: AbstractConfig,
alexa_entity: AlexaEntity,
alexa_properties: list[dict[str, Any]],
*,
invalidate_access_token: bool = True,
) -> None:
"""Send a ChangeReport message for an Alexa entity. """Send a ChangeReport message for an Alexa entity.
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events
@ -132,11 +345,11 @@ async def async_send_changereport_message(
) )
return return
headers = {"Authorization": f"Bearer {token}"} headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
endpoint = alexa_entity.alexa_id() endpoint = alexa_entity.alexa_id()
payload = { payload: dict[str, Any] = {
API_CHANGE: { API_CHANGE: {
"cause": {"type": Cause.APP_INTERACTION}, "cause": {"type": Cause.APP_INTERACTION},
"properties": alexa_properties, "properties": alexa_properties,
@ -149,8 +362,9 @@ async def async_send_changereport_message(
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
try: try:
async with async_timeout.timeout(DEFAULT_TIMEOUT): async with timeout(DEFAULT_TIMEOUT):
response = await session.post( response = await session.post(
config.endpoint, config.endpoint,
headers=headers, headers=headers,
@ -203,9 +417,9 @@ async def async_send_add_or_update_message(
""" """
token = await config.async_get_access_token() token = await config.async_get_access_token()
headers = {"Authorization": f"Bearer {token}"} headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
endpoints = [] endpoints: list[dict[str, Any]] = []
for entity_id in entity_ids: for entity_id in entity_ids:
if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS: if (domain := entity_id.split(".", 1)[0]) not in ENTITY_ADAPTERS:
@ -217,7 +431,10 @@ async def async_send_add_or_update_message(
alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state) alexa_entity = ENTITY_ADAPTERS[domain](hass, config, state)
endpoints.append(alexa_entity.serialize_discovery()) endpoints.append(alexa_entity.serialize_discovery())
payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} payload: dict[str, Any] = {
"endpoints": endpoints,
"scope": {"type": "BearerToken", "token": token},
}
message = AlexaResponse( message = AlexaResponse(
name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload
@ -226,6 +443,7 @@ async def async_send_add_or_update_message(
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
return await session.post( return await session.post(
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
) )
@ -240,9 +458,9 @@ async def async_send_delete_message(
""" """
token = await config.async_get_access_token() token = await config.async_get_access_token()
headers = {"Authorization": f"Bearer {token}"} headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
endpoints = [] endpoints: list[dict[str, Any]] = []
for entity_id in entity_ids: for entity_id in entity_ids:
domain = entity_id.split(".", 1)[0] domain = entity_id.split(".", 1)[0]
@ -252,7 +470,10 @@ async def async_send_delete_message(
endpoints.append({"endpointId": generate_alexa_id(entity_id)}) endpoints.append({"endpointId": generate_alexa_id(entity_id)})
payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} payload: dict[str, Any] = {
"endpoints": endpoints,
"scope": {"type": "BearerToken", "token": token},
}
message = AlexaResponse( message = AlexaResponse(
name="DeleteReport", namespace="Alexa.Discovery", payload=payload name="DeleteReport", namespace="Alexa.Discovery", payload=payload
@ -261,19 +482,22 @@ async def async_send_delete_message(
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
return await session.post( return await session.post(
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
) )
async def async_send_doorbell_event_message(hass, config, alexa_entity): async def async_send_doorbell_event_message(
hass: HomeAssistant, config: AbstractConfig, alexa_entity: AlexaEntity
) -> None:
"""Send a DoorbellPress event message for an Alexa entity. """Send a DoorbellPress event message for an Alexa entity.
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html
""" """
token = await config.async_get_access_token() token = await config.async_get_access_token()
headers = {"Authorization": f"Bearer {token}"} headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
endpoint = alexa_entity.alexa_id() endpoint = alexa_entity.alexa_id()
@ -291,8 +515,9 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
message_serialized = message.serialize() message_serialized = message.serialize()
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
assert config.endpoint is not None
try: try:
async with async_timeout.timeout(DEFAULT_TIMEOUT): async with timeout(DEFAULT_TIMEOUT):
response = await session.post( response = await session.post(
config.endpoint, config.endpoint,
headers=headers, headers=headers,

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/amazon_polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"], "loggers": ["boto3", "botocore", "s3transfer"],
"requirements": ["boto3==1.20.24"] "requirements": ["boto3==1.28.17"]
} }

View File

@ -24,7 +24,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

View File

@ -100,7 +100,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
token_info = await oauth.get_access_token(code) token_info = await oauth.get_access_token(code)
except ambiclimate.AmbiclimateOauthError: except ambiclimate.AmbiclimateOauthError:
_LOGGER.error("Failed to get access token", exc_info=True) _LOGGER.exception("Failed to get access token")
return None return None
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY) store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)

View File

@ -5,7 +5,6 @@ from typing import Any
from aioambient import Websocket from aioambient import Websocket
from aioambient.errors import WebsocketError from aioambient.errors import WebsocketError
from aioambient.util import get_public_device_id
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -19,11 +18,7 @@ from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import async_dispatcher_send
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
from .const import ( from .const import (
@ -148,6 +143,7 @@ class AmbientStation:
"""Define a handler to fire when the data is received.""" """Define a handler to fire when the data is received."""
mac = data["macAddress"] mac = data["macAddress"]
# If data has not changed, don't update:
if data == self.stations[mac][ATTR_LAST_DATA]: if data == self.stations[mac][ATTR_LAST_DATA]:
return return
@ -196,71 +192,3 @@ class AmbientStation:
async def ws_disconnect(self) -> None: async def ws_disconnect(self) -> None:
"""Disconnect from the websocket.""" """Disconnect from the websocket."""
await self.websocket.disconnect() await self.websocket.disconnect()
class AmbientWeatherEntity(Entity):
"""Define a base Ambient PWS entity."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
ambient: AmbientStation,
mac_address: str,
station_name: str,
description: EntityDescription,
) -> None:
"""Initialize the entity."""
self._ambient = ambient
public_device_id = get_public_device_id(mac_address)
self._attr_device_info = DeviceInfo(
configuration_url=(
f"https://ambientweather.net/dashboard/{public_device_id}"
),
identifiers={(DOMAIN, mac_address)},
manufacturer="Ambient Weather",
name=station_name.capitalize(),
)
self._attr_unique_id = f"{mac_address}_{description.key}"
self._mac_address = mac_address
self.entity_description = description
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@callback
def update() -> None:
"""Update the state."""
if self.entity_description.key == TYPE_SOLARRADIATION_LX:
self._attr_available = (
self._ambient.stations[self._mac_address][ATTR_LAST_DATA][
TYPE_SOLARRADIATION
]
is not None
)
else:
self._attr_available = (
self._ambient.stations[self._mac_address][ATTR_LAST_DATA][
self.entity_description.key
]
is not None
)
self.update_from_latest_data()
self.async_write_ha_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass, f"ambient_station_data_update_{self._mac_address}", update
)
)
self.update_from_latest_data()
@callback
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
raise NotImplementedError

View File

@ -14,8 +14,8 @@ from homeassistant.const import ATTR_NAME, EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AmbientWeatherEntity
from .const import ATTR_LAST_DATA, DOMAIN from .const import ATTR_LAST_DATA, DOMAIN
from .entity import AmbientWeatherEntity
TYPE_BATT1 = "batt1" TYPE_BATT1 = "batt1"
TYPE_BATT10 = "batt10" TYPE_BATT10 = "batt10"
@ -80,304 +80,303 @@ class AmbientBinarySensorDescription(
BINARY_SENSOR_DESCRIPTIONS = ( BINARY_SENSOR_DESCRIPTIONS = (
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATTOUT, key=TYPE_BATTOUT,
name="Battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT1, key=TYPE_BATT1,
name="Battery 1", translation_key="battery_1",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT2, key=TYPE_BATT2,
name="Battery 2", translation_key="battery_2",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT3, key=TYPE_BATT3,
name="Battery 3", translation_key="battery_3",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT4, key=TYPE_BATT4,
name="Battery 4", translation_key="battery_4",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT5, key=TYPE_BATT5,
name="Battery 5", translation_key="battery_5",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT6, key=TYPE_BATT6,
name="Battery 6", translation_key="battery_6",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT7, key=TYPE_BATT7,
name="Battery 7", translation_key="battery_7",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT8, key=TYPE_BATT8,
name="Battery 8", translation_key="battery_8",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT9, key=TYPE_BATT9,
name="Battery 9", translation_key="battery_9",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATTIN, key=TYPE_BATTIN,
name="Interior battery", translation_key="interior_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT10, key=TYPE_BATT10,
name="Battery 10", translation_key="battery_10",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK1, key=TYPE_BATT_LEAK1,
name="Leak detector battery 1", translation_key="leak_detector_battery_1",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK2, key=TYPE_BATT_LEAK2,
name="Leak detector battery 2", translation_key="leak_detector_battery_2",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK3, key=TYPE_BATT_LEAK3,
name="Leak detector battery 3", translation_key="leak_detector_battery_3",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LEAK4, key=TYPE_BATT_LEAK4,
name="Leak detector battery 4", translation_key="leak_detector_battery_4",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM1, key=TYPE_BATT_SM1,
name="Soil monitor battery 1", translation_key="soil_monitor_battery_1",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM2, key=TYPE_BATT_SM2,
name="Soil monitor battery 2", translation_key="soil_monitor_battery_2",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM3, key=TYPE_BATT_SM3,
name="Soil monitor battery 3", translation_key="soil_monitor_battery_3",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM4, key=TYPE_BATT_SM4,
name="Soil monitor battery 4", translation_key="soil_monitor_battery_4",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM5, key=TYPE_BATT_SM5,
name="Soil monitor battery 5", translation_key="soil_monitor_battery_5",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM6, key=TYPE_BATT_SM6,
name="Soil monitor battery 6", translation_key="soil_monitor_battery_6",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM7, key=TYPE_BATT_SM7,
name="Soil monitor battery 7", translation_key="soil_monitor_battery_7",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM8, key=TYPE_BATT_SM8,
name="Soil monitor battery 8", translation_key="soil_monitor_battery_8",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM9, key=TYPE_BATT_SM9,
name="Soil monitor battery 9", translation_key="soil_monitor_battery_9",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_SM10, key=TYPE_BATT_SM10,
name="Soil monitor battery 10", translation_key="soil_monitor_battery_10",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_CO2, key=TYPE_BATT_CO2,
name="CO2 battery", translation_key="co2_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT_LIGHTNING, key=TYPE_BATT_LIGHTNING,
name="Lightning detector battery", translation_key="lightning_detector_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK1, key=TYPE_LEAK1,
name="Leak detector 1", translation_key="leak_detector_1",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK2, key=TYPE_LEAK2,
name="Leak detector 2", translation_key="leak_detector_2",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK3, key=TYPE_LEAK3,
name="Leak detector 3", translation_key="leak_detector_3",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_LEAK4, key=TYPE_LEAK4,
name="Leak detector 4", translation_key="leak_detector_4",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_PM25IN_BATT, key=TYPE_PM25IN_BATT,
name="PM25 indoor battery", translation_key="pm25_indoor_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_PM25_BATT, key=TYPE_PM25_BATT,
name="PM25 battery", translation_key="pm25_battery",
device_class=BinarySensorDeviceClass.BATTERY, device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY1, key=TYPE_RELAY1,
name="Relay 1", translation_key="relay_1",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY2, key=TYPE_RELAY2,
name="Relay 2", translation_key="relay_2",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY3, key=TYPE_RELAY3,
name="Relay 3", translation_key="relay_3",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY4, key=TYPE_RELAY4,
name="Relay 4", translation_key="relay_4",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY5, key=TYPE_RELAY5,
name="Relay 5", translation_key="relay_5",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY6, key=TYPE_RELAY6,
name="Relay 6", translation_key="relay_6",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY7, key=TYPE_RELAY7,
name="Relay 7", translation_key="relay_7",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY8, key=TYPE_RELAY8,
name="Relay 8", translation_key="relay_8",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY9, key=TYPE_RELAY9,
name="Relay 9", translation_key="relay_9",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
), ),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_RELAY10, key=TYPE_RELAY10,
name="Relay 10", translation_key="relay_10",
device_class=BinarySensorDeviceClass.CONNECTIVITY, device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=1, on_state=1,
@ -409,9 +408,6 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity):
@callback @callback
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Fetch new state data for the entity.""" """Fetch new state data for the entity."""
self._attr_is_on = ( description = self.entity_description
self._ambient.stations[self._mac_address][ATTR_LAST_DATA][ last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA]
self.entity_description.key self._attr_is_on = last_data[description.key] == description.on_state
]
== self.entity_description.on_state
)

View File

@ -0,0 +1,70 @@
"""Base entity Ambient Weather Station Service."""
from __future__ import annotations
from aioambient.util import get_public_device_id
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity, EntityDescription
from . import AmbientStation
from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX
class AmbientWeatherEntity(Entity):
"""Define a base Ambient PWS entity."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
ambient: AmbientStation,
mac_address: str,
station_name: str,
description: EntityDescription,
) -> None:
"""Initialize the entity."""
self._ambient = ambient
public_device_id = get_public_device_id(mac_address)
self._attr_device_info = DeviceInfo(
configuration_url=(
f"https://ambientweather.net/dashboard/{public_device_id}"
),
identifiers={(DOMAIN, mac_address)},
manufacturer="Ambient Weather",
name=station_name.capitalize(),
)
self._attr_unique_id = f"{mac_address}_{description.key}"
self._mac_address = mac_address
self.entity_description = description
@callback
def _async_update(self) -> None:
"""Update the state."""
last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA]
key = self.entity_description.key
available_key = TYPE_SOLARRADIATION if key == TYPE_SOLARRADIATION_LX else key
self._attr_available = last_data[available_key] is not None
self.update_from_latest_data()
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"ambient_station_data_update_{self._mac_address}",
self._async_update,
)
)
self.update_from_latest_data()
@callback
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
raise NotImplementedError

View File

@ -28,8 +28,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AmbientStation, AmbientWeatherEntity from . import AmbientStation
from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX
from .entity import AmbientWeatherEntity
TYPE_24HOURRAININ = "24hourrainin" TYPE_24HOURRAININ = "24hourrainin"
TYPE_AQI_PM25 = "aqi_pm25" TYPE_AQI_PM25 = "aqi_pm25"
@ -113,544 +114,536 @@ TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_DESCRIPTIONS = ( SENSOR_DESCRIPTIONS = (
SensorEntityDescription( SensorEntityDescription(
key=TYPE_24HOURRAININ, key=TYPE_24HOURRAININ,
name="24 hr rain", translation_key="24_hour_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25, key=TYPE_AQI_PM25,
name="AQI PM2.5", translation_key="pm25_aqi",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25_24H, key=TYPE_AQI_PM25_24H,
name="AQI PM2.5 24h avg", translation_key="pm25_aqi_24h_average",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25_IN, key=TYPE_AQI_PM25_IN,
name="AQI PM2.5 indoor", translation_key="pm25_indoor_aqi",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_AQI_PM25_IN_24H, key=TYPE_AQI_PM25_IN_24H,
name="AQI PM2.5 indoor 24h avg", translation_key="pm25_indoor_aqi_24h_average",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_BAROMABSIN, key=TYPE_BAROMABSIN,
name="Abs pressure", translation_key="absolute_pressure",
native_unit_of_measurement=UnitOfPressure.INHG, native_unit_of_measurement=UnitOfPressure.INHG,
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_BAROMRELIN, key=TYPE_BAROMRELIN,
name="Rel pressure", translation_key="relative_pressure",
native_unit_of_measurement=UnitOfPressure.INHG, native_unit_of_measurement=UnitOfPressure.INHG,
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_CO2, key=TYPE_CO2,
name="CO2",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2, device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_DAILYRAININ, key=TYPE_DAILYRAININ,
name="Daily rain", translation_key="daily_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_DEWPOINT, key=TYPE_DEWPOINT,
name="Dew point", translation_key="dew_point",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_EVENTRAININ, key=TYPE_EVENTRAININ,
name="Event rain", translation_key="event_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_FEELSLIKE, key=TYPE_FEELSLIKE,
name="Feels like", translation_key="feels_like",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HOURLYRAININ, key=TYPE_HOURLYRAININ,
name="Hourly rain rate",
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR, native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY10, key=TYPE_HUMIDITY10,
name="Humidity 10", translation_key="humidity_10",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY1, key=TYPE_HUMIDITY1,
name="Humidity 1", translation_key="humidity_1",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY2, key=TYPE_HUMIDITY2,
name="Humidity 2", translation_key="humidity_2",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY3, key=TYPE_HUMIDITY3,
name="Humidity 3", translation_key="humidity_3",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY4, key=TYPE_HUMIDITY4,
name="Humidity 4", translation_key="humidity_4",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY5, key=TYPE_HUMIDITY5,
name="Humidity 5", translation_key="humidity_5",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY6, key=TYPE_HUMIDITY6,
name="Humidity 6", translation_key="humidity_6",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY7, key=TYPE_HUMIDITY7,
name="Humidity 7", translation_key="humidity_7",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY8, key=TYPE_HUMIDITY8,
name="Humidity 8", translation_key="humidity_8",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY9, key=TYPE_HUMIDITY9,
name="Humidity 9", translation_key="humidity_9",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITY, key=TYPE_HUMIDITY,
name="Humidity",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_HUMIDITYIN, key=TYPE_HUMIDITYIN,
name="Humidity in", translation_key="humidity_indoor",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_LASTRAIN, key=TYPE_LASTRAIN,
name="Last rain", translation_key="last_rain",
icon="mdi:water", icon="mdi:water",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_LIGHTNING_PER_DAY, key=TYPE_LIGHTNING_PER_DAY,
name="Lightning strikes per day", translation_key="lightning_strikes_per_day",
icon="mdi:lightning-bolt", icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes", native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_LIGHTNING_PER_HOUR, key=TYPE_LIGHTNING_PER_HOUR,
name="Lightning strikes per hour", translation_key="lightning_strikes_per_hour",
icon="mdi:lightning-bolt", icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes", native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_MAXDAILYGUST, key=TYPE_MAXDAILYGUST,
name="Max gust", translation_key="max_gust",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_MONTHLYRAININ, key=TYPE_MONTHLYRAININ,
name="Monthly rain", translation_key="monthly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25_24H, key=TYPE_PM25_24H,
name="PM25 24h avg", translation_key="pm25_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25_IN, key=TYPE_PM25_IN,
name="PM25 indoor", translation_key="pm25_indoor",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25_IN_24H, key=TYPE_PM25_IN_24H,
name="PM25 indoor 24h avg", translation_key="pm25_indoor_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_PM25, key=TYPE_PM25,
name="PM25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM10, key=TYPE_SOILHUM10,
name="Soil humidity 10", translation_key="soil_humidity_10",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM1, key=TYPE_SOILHUM1,
name="Soil humidity 1", translation_key="soil_humidity_1",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM2, key=TYPE_SOILHUM2,
name="Soil humidity 2", translation_key="soil_humidity_2",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM3, key=TYPE_SOILHUM3,
name="Soil humidity 3", translation_key="soil_humidity_3",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM4, key=TYPE_SOILHUM4,
name="Soil humidity 4", translation_key="soil_humidity_4",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM5, key=TYPE_SOILHUM5,
name="Soil humidity 5", translation_key="soil_humidity_5",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM6, key=TYPE_SOILHUM6,
name="Soil humidity 6", translation_key="soil_humidity_6",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM7, key=TYPE_SOILHUM7,
name="Soil humidity 7", translation_key="soil_humidity_7",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM8, key=TYPE_SOILHUM8,
name="Soil humidity 8", translation_key="soil_humidity_8",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILHUM9, key=TYPE_SOILHUM9,
name="Soil humidity 9", translation_key="soil_humidity_9",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP10F, key=TYPE_SOILTEMP10F,
name="Soil temp 10", translation_key="soil_temperature_10",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP1F, key=TYPE_SOILTEMP1F,
name="Soil temp 1", translation_key="soil_temperature_1",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP2F, key=TYPE_SOILTEMP2F,
name="Soil temp 2", translation_key="soil_temperature_2",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP3F, key=TYPE_SOILTEMP3F,
name="Soil temp 3", translation_key="soil_temperature_3",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP4F, key=TYPE_SOILTEMP4F,
name="Soil temp 4", translation_key="soil_temperature_4",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP5F, key=TYPE_SOILTEMP5F,
name="Soil temp 5", translation_key="soil_temperature_5",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP6F, key=TYPE_SOILTEMP6F,
name="Soil temp 6", translation_key="soil_temperature_6",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP7F, key=TYPE_SOILTEMP7F,
name="Soil temp 7", translation_key="soil_temperature_7",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP8F, key=TYPE_SOILTEMP8F,
name="Soil temp 8", translation_key="soil_temperature_8",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOILTEMP9F, key=TYPE_SOILTEMP9F,
name="Soil temp 9", translation_key="soil_temperature_9",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOLARRADIATION, key=TYPE_SOLARRADIATION,
name="Solar rad",
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
device_class=SensorDeviceClass.IRRADIANCE, device_class=SensorDeviceClass.IRRADIANCE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_SOLARRADIATION_LX, key=TYPE_SOLARRADIATION_LX,
name="Solar rad",
native_unit_of_measurement=LIGHT_LUX, native_unit_of_measurement=LIGHT_LUX,
device_class=SensorDeviceClass.ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP10F, key=TYPE_TEMP10F,
name="Temp 10", translation_key="temperature_10",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP1F, key=TYPE_TEMP1F,
name="Temp 1", translation_key="temperature_1",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP2F, key=TYPE_TEMP2F,
name="Temp 2", translation_key="temperature_2",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP3F, key=TYPE_TEMP3F,
name="Temp 3", translation_key="temperature_3",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP4F, key=TYPE_TEMP4F,
name="Temp 4", translation_key="temperature_4",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP5F, key=TYPE_TEMP5F,
name="Temp 5", translation_key="temperature_5",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP6F, key=TYPE_TEMP6F,
name="Temp 6", translation_key="temperature_6",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP7F, key=TYPE_TEMP7F,
name="Temp 7", translation_key="temperature_7",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP8F, key=TYPE_TEMP8F,
name="Temp 8", translation_key="temperature_8",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMP9F, key=TYPE_TEMP9F,
name="Temp 9", translation_key="temperature_9",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMPF, key=TYPE_TEMPF,
name="Temp",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TEMPINF, key=TYPE_TEMPINF,
name="Inside temp", translation_key="inside_temperature",
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_TOTALRAININ, key=TYPE_TOTALRAININ,
name="Lifetime rain", translation_key="lifetime_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_UV, key=TYPE_UV,
name="UV index", translation_key="uv_index",
native_unit_of_measurement="Index", native_unit_of_measurement="Index",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WEEKLYRAININ, key=TYPE_WEEKLYRAININ,
name="Weekly rain", translation_key="weekly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL, state_class=SensorStateClass.TOTAL,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR, key=TYPE_WINDDIR,
name="Wind dir", translation_key="wind_direction",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR_AVG10M, key=TYPE_WINDDIR_AVG10M,
name="Wind dir avg 10m", translation_key="wind_direction_average_10m",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR_AVG2M, key=TYPE_WINDDIR_AVG2M,
name="Wind dir avg 2m", translation_key="wind_direction_average_2m",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDGUSTDIR, key=TYPE_WINDGUSTDIR,
name="Gust dir", translation_key="wind_gust_direction",
icon="mdi:weather-windy", icon="mdi:weather-windy",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDGUSTMPH, key=TYPE_WINDGUSTMPH,
name="Wind gust", translation_key="wind_gust",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDSPDMPH_AVG10M, key=TYPE_WINDSPDMPH_AVG10M,
name="Wind avg 10m", translation_key="wind_average_10m",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDSPDMPH_AVG2M, key=TYPE_WINDSPDMPH_AVG2M,
name="Wind avg 2m", translation_key="wind_average_2m",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDSPEEDMPH, key=TYPE_WINDSPEEDMPH,
name="Wind speed",
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_YEARLYRAININ, key=TYPE_YEARLYRAININ,
name="Yearly rain", translation_key="yearly_rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION, device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
@ -694,11 +687,9 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
@callback @callback
def update_from_latest_data(self) -> None: def update_from_latest_data(self) -> None:
"""Fetch new state data for the sensor.""" """Fetch new state data for the sensor."""
raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][ key = self.entity_description.key
self.entity_description.key raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][key]
] if key == TYPE_LASTRAIN:
if self.entity_description.key == TYPE_LASTRAIN:
self._attr_native_value = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S.%f%z") self._attr_native_value = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S.%f%z")
else: else:
self._attr_native_value = raw self._attr_native_value = raw

View File

@ -16,5 +16,356 @@
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]" "already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
} }
},
"entity": {
"binary_sensor": {
"battery_1": {
"name": "Battery 1"
},
"battery_2": {
"name": "Battery 2"
},
"battery_3": {
"name": "Battery 3"
},
"battery_4": {
"name": "Battery 4"
},
"battery_5": {
"name": "Battery 5"
},
"battery_6": {
"name": "Battery 6"
},
"battery_7": {
"name": "Battery 7"
},
"battery_8": {
"name": "Battery 8"
},
"battery_9": {
"name": "Battery 9"
},
"battery_10": {
"name": "Battery 10"
},
"interior_battery": {
"name": "Interior battery"
},
"leak_detector_battery_1": {
"name": "Leak detector battery 1"
},
"leak_detector_battery_2": {
"name": "Leak detector battery 2"
},
"leak_detector_battery_3": {
"name": "Leak detector battery 3"
},
"leak_detector_battery_4": {
"name": "Leak detector battery 4"
},
"soil_monitor_battery_1": {
"name": "Soil monitor battery 1"
},
"soil_monitor_battery_2": {
"name": "Soil monitor battery 2"
},
"soil_monitor_battery_3": {
"name": "Soil monitor battery 3"
},
"soil_monitor_battery_4": {
"name": "Soil monitor battery 4"
},
"soil_monitor_battery_5": {
"name": "Soil monitor battery 5"
},
"soil_monitor_battery_6": {
"name": "Soil monitor battery 6"
},
"soil_monitor_battery_7": {
"name": "Soil monitor battery 7"
},
"soil_monitor_battery_8": {
"name": "Soil monitor battery 8"
},
"soil_monitor_battery_9": {
"name": "Soil monitor battery 9"
},
"soil_monitor_battery_10": {
"name": "Soil monitor battery 10"
},
"co2_battery": {
"name": "Carbon dioxide battery"
},
"lightning_detector_battery": {
"name": "Lightning detector battery"
},
"leak_detector_1": {
"name": "Leak detector 1"
},
"leak_detector_2": {
"name": "Leak detector 2"
},
"leak_detector_3": {
"name": "Leak detector 3"
},
"leak_detector_4": {
"name": "Leak detector 4"
},
"pm25_indoor_battery": {
"name": "PM25 indoor battery"
},
"pm25_battery": {
"name": "PM25 battery"
},
"relay_1": {
"name": "Relay 1"
},
"relay_2": {
"name": "Relay 2"
},
"relay_3": {
"name": "Relay 3"
},
"relay_4": {
"name": "Relay 4"
},
"relay_5": {
"name": "Relay 5"
},
"relay_6": {
"name": "Relay 6"
},
"relay_7": {
"name": "Relay 7"
},
"relay_8": {
"name": "Relay 8"
},
"relay_9": {
"name": "Relay 9"
},
"relay_10": {
"name": "Relay 10"
}
},
"sensor": {
"24_hour_rain": {
"name": "Rain 24 hours"
},
"pm25_aqi": {
"name": "PM2.5 AQI"
},
"pm25_aqi_24h_average": {
"name": "PM2.5 AQI 24 hour average"
},
"pm25_indoor_aqi": {
"name": "PM2.5 indoor AQI"
},
"pm25_indoor_aqi_24h_average": {
"name": "PM2.5 indoor AQI"
},
"absolute_pressure": {
"name": "Absolute pressure"
},
"relative_pressure": {
"name": "Relative pressure"
},
"daily_rain": {
"name": "Daily rain"
},
"dew_point": {
"name": "Dew point"
},
"event_rain": {
"name": "Event rain"
},
"feels_like": {
"name": "Feels like"
},
"humidity_1": {
"name": "Humidity 1"
},
"humidity_2": {
"name": "Humidity 2"
},
"humidity_3": {
"name": "Humidity 3"
},
"humidity_4": {
"name": "Humidity 4"
},
"humidity_5": {
"name": "Humidity 5"
},
"humidity_6": {
"name": "Humidity 6"
},
"humidity_7": {
"name": "Humidity 7"
},
"humidity_8": {
"name": "Humidity 8"
},
"humidity_9": {
"name": "Humidity 9"
},
"humidity_10": {
"name": "Humidity 10"
},
"humidity_indoor": {
"name": "Humidity indoor"
},
"last_rain": {
"name": "Last rain"
},
"lightning_strikes_per_day": {
"name": "Lightning strikes per day"
},
"lightning_strikes_per_hour": {
"name": "Lightning strikes per hour"
},
"max_gust": {
"name": "Max gust"
},
"monthly_rain": {
"name": "Monthly rain"
},
"pm25_24h_average": {
"name": "PM2.5 24 hour average"
},
"pm25_indoor": {
"name": "PM2.5 indoor"
},
"pm25_indoor_24h_average": {
"name": "PM2.5 indoor 24 hour average"
},
"soil_humidity_1": {
"name": "Soil humidity 1"
},
"soil_humidity_2": {
"name": "Soil humidity 2"
},
"soil_humidity_3": {
"name": "Soil humidity 3"
},
"soil_humidity_4": {
"name": "Soil humidity 4"
},
"soil_humidity_5": {
"name": "Soil humidity 5"
},
"soil_humidity_6": {
"name": "Soil humidity 6"
},
"soil_humidity_7": {
"name": "Soil humidity 7"
},
"soil_humidity_8": {
"name": "Soil humidity 8"
},
"soil_humidity_9": {
"name": "Soil humidity 9"
},
"soil_humidity_10": {
"name": "Soil humidity 10"
},
"soil_temperature_1": {
"name": "Soil temperature 1"
},
"soil_temperature_2": {
"name": "Soil temperature 2"
},
"soil_temperature_3": {
"name": "Soil temperature 3"
},
"soil_temperature_4": {
"name": "Soil temperature 4"
},
"soil_temperature_5": {
"name": "Soil temperature 5"
},
"soil_temperature_6": {
"name": "Soil temperature 6"
},
"soil_temperature_7": {
"name": "Soil temperature 7"
},
"soil_temperature_8": {
"name": "Soil temperature 8"
},
"soil_temperature_9": {
"name": "Soil temperature 9"
},
"soil_temperature_10": {
"name": "Soil temperature 10"
},
"temperature_1": {
"name": "Temperature 1"
},
"temperature_2": {
"name": "Temperature 2"
},
"temperature_3": {
"name": "Temperature 3"
},
"temperature_4": {
"name": "Temperature 4"
},
"temperature_5": {
"name": "Temperature 5"
},
"temperature_6": {
"name": "Temperature 6"
},
"temperature_7": {
"name": "Temperature 7"
},
"temperature_8": {
"name": "Temperature 8"
},
"temperature_9": {
"name": "Temperature 9"
},
"temperature_10": {
"name": "Temperature 10"
},
"inside_temperature": {
"name": "Inside temperature"
},
"lifetime_rain": {
"name": "Lifetime rain"
},
"uv_index": {
"name": "UV index"
},
"weekly_rain": {
"name": "Weekly rain"
},
"wind_direction": {
"name": "Wind direction"
},
"wind_direction_average_10m": {
"name": "Wind direction average 10 minutes"
},
"wind_direction_average_2m": {
"name": "Wind direction average 2 minutes"
},
"wind_gust_direction": {
"name": "Wind gust direction"
},
"wind_gust": {
"name": "Wind gust"
},
"wind_average_10m": {
"name": "Wind average 10 minutes"
},
"wind_average_2m": {
"name": "Wind average 2 minutes"
},
"yearly_rain": {
"name": "Yearly rain"
}
}
} }
} }

View File

@ -2,13 +2,13 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
from dataclasses import asdict as dataclass_asdict, dataclass from dataclasses import asdict as dataclass_asdict, dataclass
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
import uuid import uuid
import aiohttp import aiohttp
import async_timeout
from homeassistant.components import hassio from homeassistant.components import hassio
from homeassistant.components.api import ATTR_INSTALLATION_TYPE from homeassistant.components.api import ATTR_INSTALLATION_TYPE
@ -22,9 +22,7 @@ from homeassistant.components.recorder import (
get_instance as get_recorder_instance, get_instance as get_recorder_instance,
) )
import homeassistant.config as conf_util import homeassistant.config as conf_util
from homeassistant.config_entries import ( from homeassistant.config_entries import SOURCE_IGNORE
SOURCE_IGNORE,
)
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -315,7 +313,7 @@ class Analytics:
) )
try: try:
async with async_timeout.timeout(30): async with timeout(30):
response = await self.session.post(self.endpoint, json=payload) response = await self.session.post(self.endpoint, json=payload)
if response.status == 200: if response.status == 200:
LOGGER.info( LOGGER.info(

View File

@ -11,7 +11,7 @@ from homeassistant.const import (
HTTP_BASIC_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN

View File

@ -1,7 +1,7 @@
"""Base class for Android IP Webcam entities.""" """Base class for Android IP Webcam entities."""
from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import DOMAIN

View File

@ -32,9 +32,8 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle from homeassistant.util import Throttle

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from asyncio import timeout
import logging import logging
from androidtvremote2 import ( from androidtvremote2 import (
@ -10,7 +11,6 @@ from androidtvremote2 import (
ConnectionClosed, ConnectionClosed,
InvalidAuth, InvalidAuth,
) )
import async_timeout
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api.add_is_available_updated_callback(is_available_updated) api.add_is_available_updated_callback(is_available_updated)
try: try:
async with async_timeout.timeout(5.0): async with timeout(5.0):
await api.async_connect() await api.async_connect()
except InvalidAuth as exc: except InvalidAuth as exc:
# The Android TV is hard reset or the certificate and key files were deleted. # The Android TV is hard reset or the certificate and key files were deleted.

View File

@ -12,8 +12,12 @@ from androidtvremote2 import (
) )
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
@ -35,7 +39,7 @@ STEP_PAIR_DATA_SCHEMA = vol.Schema(
) )
class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Android TV Remote.""" """Handle a config flow for Android TV Remote."""
VERSION = 1 VERSION = 1
@ -43,7 +47,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize a new AndroidTVRemoteConfigFlow.""" """Initialize a new AndroidTVRemoteConfigFlow."""
self.api: AndroidTVRemote | None = None self.api: AndroidTVRemote | None = None
self.reauth_entry: config_entries.ConfigEntry | None = None self.reauth_entry: ConfigEntry | None = None
self.host: str | None = None self.host: str | None = None
self.name: str | None = None self.name: str | None = None
self.mac: str | None = None self.mac: str | None = None
@ -192,19 +196,15 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow( def async_get_options_flow(
config_entry: config_entries.ConfigEntry, config_entry: ConfigEntry,
) -> config_entries.OptionsFlow: ) -> AndroidTVRemoteOptionsFlowHandler:
"""Create the options flow.""" """Create the options flow."""
return OptionsFlowHandler(config_entry) return AndroidTVRemoteOptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow): class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Android TV Remote options flow.""" """Android TV Remote options flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init( async def async_step_init(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:

View File

@ -7,8 +7,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN

View File

@ -6,7 +6,7 @@ import logging
from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
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_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device[1], device[1],
api.jwt, api.jwt,
) )
for device in entry.data["devices"] for device in entry.data[CONF_DEVICES]
] ]
try: try:
new_devices = await api.get_devices() new_devices = await api.get_devices()
@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, entry,
data={ data={
**entry.data, **entry.data,
**{"devices": serialize_device_list(devices)}, **{CONF_DEVICES: serialize_device_list(devices)},
}, },
) )
coordinators = [AnovaCoordinator(hass, device) for device in devices] coordinators = [AnovaCoordinator(hass, device) for device in devices]

View File

@ -4,16 +4,16 @@ from __future__ import annotations
from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
from .util import serialize_device_list from .util import serialize_device_list
class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN): class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
"""Sets up a config flow for Anova.""" """Sets up a config flow for Anova."""
VERSION = 1 VERSION = 1
@ -25,7 +25,7 @@ class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
api = AnovaApi( api = AnovaApi(
aiohttp_client.async_get_clientsession(self.hass), async_get_clientsession(self.hass),
user_input[CONF_USERNAME], user_input[CONF_USERNAME],
user_input[CONF_PASSWORD], user_input[CONF_PASSWORD],
) )
@ -48,7 +48,7 @@ class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN):
data={ data={
CONF_USERNAME: api.username, CONF_USERNAME: api.username,
CONF_PASSWORD: api.password, CONF_PASSWORD: api.password,
"devices": device_list, CONF_DEVICES: device_list,
}, },
) )

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