mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 14:27:07 +00:00
2023.9.0 (#99741)
This commit is contained in:
commit
f70469d6ef
@ -30,6 +30,7 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/humidifier/**
|
||||
- homeassistant/components/image/**
|
||||
- homeassistant/components/image_processing/**
|
||||
- homeassistant/components/lawn_mower/**
|
||||
- homeassistant/components/light/**
|
||||
- homeassistant/components/lock/**
|
||||
- homeassistant/components/media_player/**
|
||||
@ -54,6 +55,7 @@ base_platforms: &base_platforms
|
||||
components: &components
|
||||
- homeassistant/components/alexa/**
|
||||
- homeassistant/components/application_credentials/**
|
||||
- homeassistant/components/assist_pipeline/**
|
||||
- homeassistant/components/auth/**
|
||||
- homeassistant/components/automation/**
|
||||
- homeassistant/components/backup/**
|
||||
@ -86,6 +88,7 @@ components: &components
|
||||
- homeassistant/components/lovelace/**
|
||||
- homeassistant/components/media_source/**
|
||||
- homeassistant/components/mjpeg/**
|
||||
- homeassistant/components/modbus/**
|
||||
- homeassistant/components/mqtt/**
|
||||
- homeassistant/components/network/**
|
||||
- homeassistant/components/onboarding/**
|
||||
|
47
.coveragerc
47
.coveragerc
@ -57,6 +57,7 @@ omit =
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/__init__.py
|
||||
homeassistant/components/ambient_station/binary_sensor.py
|
||||
homeassistant/components/ambient_station/entity.py
|
||||
homeassistant/components/ambient_station/sensor.py
|
||||
homeassistant/components/amcrest/*
|
||||
homeassistant/components/ampio/*
|
||||
@ -168,6 +169,10 @@ omit =
|
||||
homeassistant/components/cmus/media_player.py
|
||||
homeassistant/components/coinbase/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/concord232/alarm_control_panel.py
|
||||
homeassistant/components/concord232/binary_sensor.py
|
||||
@ -212,10 +217,12 @@ omit =
|
||||
homeassistant/components/dominos/*
|
||||
homeassistant/components/doods/*
|
||||
homeassistant/components/doorbird/__init__.py
|
||||
homeassistant/components/doorbird/button.py
|
||||
homeassistant/components/doorbird/camera.py
|
||||
homeassistant/components/doorbird/button.py
|
||||
homeassistant/components/doorbird/device.py
|
||||
homeassistant/components/doorbird/entity.py
|
||||
homeassistant/components/doorbird/util.py
|
||||
homeassistant/components/doorbird/view.py
|
||||
homeassistant/components/dormakaba_dkey/__init__.py
|
||||
homeassistant/components/dormakaba_dkey/binary_sensor.py
|
||||
homeassistant/components/dormakaba_dkey/entity.py
|
||||
@ -234,6 +241,7 @@ omit =
|
||||
homeassistant/components/duotecno/entity.py
|
||||
homeassistant/components/duotecno/switch.py
|
||||
homeassistant/components/duotecno/cover.py
|
||||
homeassistant/components/duotecno/light.py
|
||||
homeassistant/components/dwd_weather_warnings/const.py
|
||||
homeassistant/components/dwd_weather_warnings/coordinator.py
|
||||
homeassistant/components/dwd_weather_warnings/sensor.py
|
||||
@ -270,6 +278,7 @@ omit =
|
||||
homeassistant/components/electric_kiwi/oauth2.py
|
||||
homeassistant/components/electric_kiwi/sensor.py
|
||||
homeassistant/components/electric_kiwi/coordinator.py
|
||||
homeassistant/components/electric_kiwi/select.py
|
||||
homeassistant/components/eliqonline/sensor.py
|
||||
homeassistant/components/elkm1/__init__.py
|
||||
homeassistant/components/elkm1/alarm_control_panel.py
|
||||
@ -282,7 +291,8 @@ omit =
|
||||
homeassistant/components/elmax/alarm_control_panel.py
|
||||
homeassistant/components/elmax/binary_sensor.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/elv/*
|
||||
homeassistant/components/emby/media_player.py
|
||||
@ -299,7 +309,13 @@ omit =
|
||||
homeassistant/components/enocean/sensor.py
|
||||
homeassistant/components/enocean/switch.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/switch.py
|
||||
homeassistant/components/entur_public_transport/*
|
||||
homeassistant/components/environment_canada/__init__.py
|
||||
homeassistant/components/environment_canada/camera.py
|
||||
@ -334,6 +350,7 @@ omit =
|
||||
homeassistant/components/ezviz/entity.py
|
||||
homeassistant/components/ezviz/select.py
|
||||
homeassistant/components/ezviz/sensor.py
|
||||
homeassistant/components/ezviz/siren.py
|
||||
homeassistant/components/ezviz/switch.py
|
||||
homeassistant/components/ezviz/update.py
|
||||
homeassistant/components/faa_delays/__init__.py
|
||||
@ -415,6 +432,7 @@ omit =
|
||||
homeassistant/components/garadget/cover.py
|
||||
homeassistant/components/garages_amsterdam/__init__.py
|
||||
homeassistant/components/garages_amsterdam/binary_sensor.py
|
||||
homeassistant/components/garages_amsterdam/entity.py
|
||||
homeassistant/components/garages_amsterdam/sensor.py
|
||||
homeassistant/components/gc100/*
|
||||
homeassistant/components/geniushub/*
|
||||
@ -708,8 +726,6 @@ omit =
|
||||
homeassistant/components/meteoclimatic/__init__.py
|
||||
homeassistant/components/meteoclimatic/sensor.py
|
||||
homeassistant/components/meteoclimatic/weather.py
|
||||
homeassistant/components/metoffice/sensor.py
|
||||
homeassistant/components/metoffice/weather.py
|
||||
homeassistant/components/microsoft/tts.py
|
||||
homeassistant/components/mikrotik/hub.py
|
||||
homeassistant/components/mill/climate.py
|
||||
@ -764,10 +780,12 @@ omit =
|
||||
homeassistant/components/neato/__init__.py
|
||||
homeassistant/components/neato/api.py
|
||||
homeassistant/components/neato/camera.py
|
||||
homeassistant/components/neato/entity.py
|
||||
homeassistant/components/neato/hub.py
|
||||
homeassistant/components/neato/sensor.py
|
||||
homeassistant/components/neato/switch.py
|
||||
homeassistant/components/neato/vacuum.py
|
||||
homeassistant/components/neato/button.py
|
||||
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
||||
homeassistant/components/netdata/sensor.py
|
||||
homeassistant/components/netgear/__init__.py
|
||||
@ -859,7 +877,6 @@ omit =
|
||||
homeassistant/components/openhome/const.py
|
||||
homeassistant/components/openhome/media_player.py
|
||||
homeassistant/components/opensensemap/air_quality.py
|
||||
homeassistant/components/opensky/sensor.py
|
||||
homeassistant/components/opentherm_gw/__init__.py
|
||||
homeassistant/components/opentherm_gw/binary_sensor.py
|
||||
homeassistant/components/opentherm_gw/climate.py
|
||||
@ -989,6 +1006,7 @@ omit =
|
||||
homeassistant/components/renson/const.py
|
||||
homeassistant/components/renson/entity.py
|
||||
homeassistant/components/renson/sensor.py
|
||||
homeassistant/components/renson/binary_sensor.py
|
||||
homeassistant/components/raspyrfm/*
|
||||
homeassistant/components/recollect_waste/sensor.py
|
||||
homeassistant/components/recorder/repack.py
|
||||
@ -1167,7 +1185,13 @@ omit =
|
||||
homeassistant/components/squeezebox/__init__.py
|
||||
homeassistant/components/squeezebox/browse_media.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/device_tracker.py
|
||||
homeassistant/components/starlink/sensor.py
|
||||
homeassistant/components/starlink/switch.py
|
||||
homeassistant/components/starline/__init__.py
|
||||
homeassistant/components/starline/account.py
|
||||
homeassistant/components/starline/binary_sensor.py
|
||||
@ -1311,9 +1335,6 @@ omit =
|
||||
homeassistant/components/tplink_omada/__init__.py
|
||||
homeassistant/components/tplink_omada/binary_sensor.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/traccar/device_tracker.py
|
||||
homeassistant/components/tractive/__init__.py
|
||||
@ -1333,9 +1354,11 @@ omit =
|
||||
homeassistant/components/trafikverket_train/__init__.py
|
||||
homeassistant/components/trafikverket_train/coordinator.py
|
||||
homeassistant/components/trafikverket_train/sensor.py
|
||||
homeassistant/components/trafikverket_train/util.py
|
||||
homeassistant/components/trafikverket_weatherstation/__init__.py
|
||||
homeassistant/components/trafikverket_weatherstation/coordinator.py
|
||||
homeassistant/components/trafikverket_weatherstation/sensor.py
|
||||
homeassistant/components/transmission/__init__.py
|
||||
homeassistant/components/transmission/sensor.py
|
||||
homeassistant/components/transmission/switch.py
|
||||
homeassistant/components/travisci/sensor.py
|
||||
@ -1418,6 +1441,10 @@ omit =
|
||||
homeassistant/components/vlc/media_player.py
|
||||
homeassistant/components/vlc_telnet/__init__.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/volumio/__init__.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/binary_sensor.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/lock.py
|
||||
homeassistant/components/yalexs_ble/__init__.py
|
||||
@ -1502,6 +1528,9 @@ omit =
|
||||
homeassistant/components/yamaha_musiccast/select.py
|
||||
homeassistant/components/yamaha_musiccast/switch.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/yi/camera.py
|
||||
homeassistant/components/yolink/__init__.py
|
||||
|
17
.github/workflows/builder.yml
vendored
17
.github/workflows/builder.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -56,7 +56,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -98,7 +98,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
@ -197,7 +197,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.06.1
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -251,9 +251,10 @@ jobs:
|
||||
- raspberrypi4-64
|
||||
- tinker
|
||||
- yellow
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
@ -274,7 +275,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.06.1
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@ -292,7 +293,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@ -330,7 +331,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.1.1
|
||||
|
136
.github/workflows/ci.yaml
vendored
136
.github/workflows/ci.yaml
vendored
@ -19,6 +19,10 @@ on:
|
||||
description: "Skip pytest"
|
||||
default: false
|
||||
type: boolean
|
||||
skip-coverage:
|
||||
description: "Skip coverage"
|
||||
default: false
|
||||
type: boolean
|
||||
pylint-only:
|
||||
description: "Only run pylint"
|
||||
default: false
|
||||
@ -32,7 +36,7 @@ env:
|
||||
CACHE_VERSION: 5
|
||||
PIP_CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 4
|
||||
HA_SHORT_VERSION: 2023.8
|
||||
HA_SHORT_VERSION: 2023.9
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
ALL_PYTHON_VERSIONS: "['3.11']"
|
||||
# 10.3 is the oldest supported version
|
||||
@ -79,10 +83,11 @@ jobs:
|
||||
test_groups: ${{ steps.info.outputs.test_groups }}
|
||||
tests_glob: ${{ steps.info.outputs.tests_glob }}
|
||||
tests: ${{ steps.info.outputs.tests }}
|
||||
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- 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
|
||||
id: generate_python_cache_key
|
||||
run: >-
|
||||
@ -127,6 +132,7 @@ jobs:
|
||||
test_group_count=10
|
||||
tests="[]"
|
||||
tests_glob=""
|
||||
skip_coverage=""
|
||||
|
||||
if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]];
|
||||
then
|
||||
@ -176,6 +182,12 @@ jobs:
|
||||
test_full_suite="true"
|
||||
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
|
||||
echo "mariadb_groups: ${mariadb_groups}"
|
||||
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
|
||||
@ -195,6 +207,8 @@ jobs:
|
||||
echo "tests=${tests}" >> $GITHUB_OUTPUT
|
||||
echo "tests_glob: ${tests_glob}"
|
||||
echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT
|
||||
echo "skip_coverage: ${skip_coverage}"
|
||||
echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT
|
||||
|
||||
pre-commit:
|
||||
name: Prepare pre-commit base
|
||||
@ -206,7 +220,7 @@ jobs:
|
||||
- info
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -251,7 +265,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v4.7.0
|
||||
id: python
|
||||
@ -297,7 +311,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v4.7.0
|
||||
id: python
|
||||
@ -346,7 +360,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v4.7.0
|
||||
id: python
|
||||
@ -440,7 +454,7 @@ jobs:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -508,7 +522,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -540,7 +554,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -573,7 +587,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -617,7 +631,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -699,7 +713,7 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -734,9 +748,19 @@ jobs:
|
||||
- name: Run pytest (fully)
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
timeout-minutes: 60
|
||||
id: pytest-full
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
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 \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
@ -745,37 +769,54 @@ jobs:
|
||||
--dist=loadfile \
|
||||
--test-group-count ${{ needs.info.outputs.test_group_count }} \
|
||||
--test-group=${{ matrix.group }} \
|
||||
--cov="homeassistant" \
|
||||
--cov-report=xml \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
-p no:sugar \
|
||||
tests
|
||||
tests \
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
|
||||
- name: Run pytest (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
timeout-minutes: 10
|
||||
id: pytest-partial
|
||||
shell: bash
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
set -o pipefail
|
||||
|
||||
if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then
|
||||
echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py"
|
||||
exit 1
|
||||
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 \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
-n auto \
|
||||
--cov="homeassistant.components.${{ matrix.group }}" \
|
||||
--cov-report=xml \
|
||||
--cov-report=term-missing \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=0 \
|
||||
--durations-min=1 \
|
||||
-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
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
@ -824,7 +865,7 @@ jobs:
|
||||
ffmpeg \
|
||||
libmariadb-dev-compat
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -862,18 +903,27 @@ jobs:
|
||||
python3 -m script.translations develop --all
|
||||
- name: Run pytest (partially)
|
||||
timeout-minutes: 20
|
||||
id: pytest-partial
|
||||
shell: bash
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
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 \
|
||||
-qq \
|
||||
--timeout=20 \
|
||||
-n 1 \
|
||||
--cov="homeassistant.components.recorder" \
|
||||
--cov-report=xml \
|
||||
--cov-report=term-missing \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=10 \
|
||||
-p no:sugar \
|
||||
@ -881,8 +931,16 @@ jobs:
|
||||
tests/components/history \
|
||||
tests/components/logbook \
|
||||
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
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-mariadb
|
||||
@ -931,7 +989,7 @@ jobs:
|
||||
ffmpeg \
|
||||
postgresql-server-dev-14
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.7.0
|
||||
@ -969,18 +1027,27 @@ jobs:
|
||||
python3 -m script.translations develop --all
|
||||
- name: Run pytest (partially)
|
||||
timeout-minutes: 20
|
||||
id: pytest-partial
|
||||
shell: bash
|
||||
env:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
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 \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
-n 1 \
|
||||
--cov="homeassistant.components.recorder" \
|
||||
--cov-report=xml \
|
||||
--cov-report=term-missing \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
--durations=0 \
|
||||
--durations-min=10 \
|
||||
@ -989,8 +1056,16 @@ jobs:
|
||||
tests/components/history \
|
||||
tests/components/logbook \
|
||||
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
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-postgresql
|
||||
@ -1001,6 +1076,7 @@ jobs:
|
||||
|
||||
coverage:
|
||||
name: Upload test coverage to Codecov
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
@ -1008,7 +1084,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Upload coverage to Codecov (full coverage)
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.7.0
|
||||
|
6
.github/workflows/wheels.yml
vendored
6
.github/workflows/wheels.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
@ -84,7 +84,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v3
|
||||
@ -122,7 +122,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
uses: actions/checkout@v3.6.0
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v3
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -67,6 +67,7 @@ htmlcov/
|
||||
test-reports/
|
||||
test-results.xml
|
||||
test-output.xml
|
||||
pytest-*.txt
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -1,11 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.280
|
||||
rev: v0.0.285
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- repo: https://github.com/psf/black
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
@ -43,7 +43,7 @@ repos:
|
||||
hooks:
|
||||
- id: prettier
|
||||
- repo: https://github.com/cdce8p/python-typing-update
|
||||
rev: v0.5.0
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
# Run `python-typing-update` hook manually from time to time
|
||||
# to update python typing syntax.
|
||||
@ -52,7 +52,7 @@ repos:
|
||||
- id: python-typing-update
|
||||
stages: [manual]
|
||||
args:
|
||||
- --py310-plus
|
||||
- --py311-plus
|
||||
- --force
|
||||
- --keep-updates
|
||||
files: ^(homeassistant|tests|script)/.+\.py$
|
||||
|
@ -53,6 +53,7 @@ homeassistant.components.airzone_cloud.*
|
||||
homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.ambient_station.*
|
||||
homeassistant.components.amcrest.*
|
||||
@ -103,6 +104,7 @@ homeassistant.components.dhcp.*
|
||||
homeassistant.components.diagnostics.*
|
||||
homeassistant.components.dlna_dmr.*
|
||||
homeassistant.components.dnsip.*
|
||||
homeassistant.components.doorbird.*
|
||||
homeassistant.components.dormakaba_dkey.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.dunehd.*
|
||||
@ -147,6 +149,7 @@ homeassistant.components.history.*
|
||||
homeassistant.components.homeassistant.exposed_entities
|
||||
homeassistant.components.homeassistant.triggers.event
|
||||
homeassistant.components.homeassistant_alerts.*
|
||||
homeassistant.components.homeassistant_green.*
|
||||
homeassistant.components.homeassistant_hardware.*
|
||||
homeassistant.components.homeassistant_sky_connect.*
|
||||
homeassistant.components.homeassistant_yellow.*
|
||||
@ -181,6 +184,7 @@ homeassistant.components.imap.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.integration.*
|
||||
homeassistant.components.ipp.*
|
||||
homeassistant.components.iqvia.*
|
||||
homeassistant.components.isy994.*
|
||||
homeassistant.components.jellyfin.*
|
||||
@ -193,6 +197,7 @@ homeassistant.components.lacrosse.*
|
||||
homeassistant.components.lacrosse_view.*
|
||||
homeassistant.components.lametric.*
|
||||
homeassistant.components.laundrify.*
|
||||
homeassistant.components.lawn_mower.*
|
||||
homeassistant.components.lcn.*
|
||||
homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.lidarr.*
|
||||
@ -209,6 +214,7 @@ homeassistant.components.luftdaten.*
|
||||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.mastodon.*
|
||||
homeassistant.components.matter.*
|
||||
homeassistant.components.media_extractor.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.media_source.*
|
||||
homeassistant.components.metoffice.*
|
||||
@ -296,6 +302,7 @@ homeassistant.components.sonarr.*
|
||||
homeassistant.components.speedtestdotnet.*
|
||||
homeassistant.components.sql.*
|
||||
homeassistant.components.ssdp.*
|
||||
homeassistant.components.starlink.*
|
||||
homeassistant.components.statistics.*
|
||||
homeassistant.components.steamist.*
|
||||
homeassistant.components.stookalert.*
|
||||
@ -321,6 +328,7 @@ homeassistant.components.tplink.*
|
||||
homeassistant.components.tplink_omada.*
|
||||
homeassistant.components.tractive.*
|
||||
homeassistant.components.tradfri.*
|
||||
homeassistant.components.trafikverket_camera.*
|
||||
homeassistant.components.trafikverket_ferry.*
|
||||
homeassistant.components.trafikverket_train.*
|
||||
homeassistant.components.trafikverket_weatherstation.*
|
||||
|
48
CODEOWNERS
48
CODEOWNERS
@ -19,8 +19,6 @@ build.json @home-assistant/supervisor
|
||||
|
||||
# Other code
|
||||
/homeassistant/scripts/check_config.py @kellerza
|
||||
/homeassistant/const.py @epenet
|
||||
/homeassistant/util/ @epenet
|
||||
|
||||
# Integrations
|
||||
/homeassistant/components/abode/ @shred86
|
||||
@ -51,8 +49,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airthings/ @danielhiversen
|
||||
/homeassistant/components/airthings_ble/ @vincegio
|
||||
/tests/components/airthings_ble/ @vincegio
|
||||
/homeassistant/components/airtouch4/ @LonePurpleWolf
|
||||
/tests/components/airtouch4/ @LonePurpleWolf
|
||||
/homeassistant/components/airvisual/ @bachya
|
||||
/tests/components/airvisual/ @bachya
|
||||
/homeassistant/components/airvisual_pro/ @bachya
|
||||
@ -211,6 +207,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/coinbase/ @tombrien
|
||||
/homeassistant/components/color_extractor/ @GenericStudent
|
||||
/tests/components/color_extractor/ @GenericStudent
|
||||
/homeassistant/components/comelit/ @chemelli74
|
||||
/tests/components/comelit/ @chemelli74
|
||||
/homeassistant/components/comfoconnect/ @michaelarnauts
|
||||
/tests/components/comfoconnect/ @michaelarnauts
|
||||
/homeassistant/components/command_line/ @gjohansson-ST
|
||||
@ -295,12 +293,10 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dsmr/ @Robbie1221 @frenck
|
||||
/homeassistant/components/dsmr_reader/ @depl0y @glodenox
|
||||
/tests/components/dsmr_reader/ @depl0y @glodenox
|
||||
/homeassistant/components/dunehd/ @bieniu
|
||||
/tests/components/dunehd/ @bieniu
|
||||
/homeassistant/components/duotecno/ @cereal2nd
|
||||
/tests/components/duotecno/ @cereal2nd
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
|
||||
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||
/homeassistant/components/dynalite/ @ziv1234
|
||||
/tests/components/dynalite/ @ziv1234
|
||||
/homeassistant/components/eafm/ @Jc2k
|
||||
@ -345,8 +341,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/enigma2/ @fbradyirl
|
||||
/homeassistant/components/enocean/ @bdurrer
|
||||
/tests/components/enocean/ @bdurrer
|
||||
/homeassistant/components/enphase_envoy/ @gtdiehl
|
||||
/tests/components/enphase_envoy/ @gtdiehl
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
|
||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
|
||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||
/homeassistant/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
|
||||
/homeassistant/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
|
||||
/tests/components/homeassistant_hardware/ @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
|
||||
/tests/components/iotawatt/ @gtdiehl @jyavenard
|
||||
/homeassistant/components/iperf3/ @rohankapoorcom
|
||||
/homeassistant/components/ipma/ @dgomes @abmantis
|
||||
/tests/components/ipma/ @dgomes @abmantis
|
||||
/homeassistant/components/ipma/ @dgomes
|
||||
/tests/components/ipma/ @dgomes
|
||||
/homeassistant/components/ipp/ @ctalkington
|
||||
/tests/components/ipp/ @ctalkington
|
||||
/homeassistant/components/iqvia/ @bachya
|
||||
@ -673,6 +671,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/launch_library/ @ludeeus @DurgNomis-drol
|
||||
/homeassistant/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
|
||||
/tests/components/lcn/ @alengwenus
|
||||
/homeassistant/components/ld2410_ble/ @930913
|
||||
@ -729,6 +729,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/mazda/ @bdr99
|
||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||
/tests/components/meater/ @Sotolotl @emontnemery
|
||||
/homeassistant/components/media_extractor/ @joostlek
|
||||
/homeassistant/components/media_player/ @home-assistant/core
|
||||
/tests/components/media_player/ @home-assistant/core
|
||||
/homeassistant/components/media_source/ @hunterjm
|
||||
@ -1028,8 +1029,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/repairs/ @home-assistant/core
|
||||
/tests/components/repairs/ @home-assistant/core
|
||||
/homeassistant/components/repetier/ @MTrab @ShadowBr0ther
|
||||
/homeassistant/components/rest/ @epenet
|
||||
/tests/components/rest/ @epenet
|
||||
/homeassistant/components/rflink/ @javicalle
|
||||
/tests/components/rflink/ @javicalle
|
||||
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||
@ -1058,8 +1057,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rss_feed_template/ @home-assistant/core
|
||||
/homeassistant/components/rtsp_to_webrtc/ @allenporter
|
||||
/tests/components/rtsp_to_webrtc/ @allenporter
|
||||
/homeassistant/components/ruckus_unleashed/ @gabe565
|
||||
/tests/components/ruckus_unleashed/ @gabe565
|
||||
/homeassistant/components/ruckus_unleashed/ @gabe565 @lanrat
|
||||
/tests/components/ruckus_unleashed/ @gabe565 @lanrat
|
||||
/homeassistant/components/ruuvi_gateway/ @akx
|
||||
/tests/components/ruuvi_gateway/ @akx
|
||||
/homeassistant/components/ruuvitag_ble/ @akx
|
||||
@ -1077,9 +1076,11 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/scene/ @home-assistant/core
|
||||
/homeassistant/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/scrape/ @fabaff @gjohansson-ST @epenet
|
||||
/tests/components/scrape/ @fabaff @gjohansson-ST @epenet
|
||||
/homeassistant/components/scrape/ @fabaff @gjohansson-ST
|
||||
/tests/components/scrape/ @fabaff @gjohansson-ST
|
||||
/homeassistant/components/screenlogic/ @dieselrabbit @bdraco
|
||||
/tests/components/screenlogic/ @dieselrabbit @bdraco
|
||||
/homeassistant/components/script/ @home-assistant/core
|
||||
@ -1226,8 +1227,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/switch_as_x/ @home-assistant/core
|
||||
/homeassistant/components/switchbee/ @jafar-atili
|
||||
/tests/components/switchbee/ @jafar-atili
|
||||
/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/homeassistant/components/switcher_kis/ @thecode
|
||||
/tests/components/switcher_kis/ @thecode
|
||||
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
||||
@ -1298,6 +1299,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/trace/ @home-assistant/core
|
||||
/homeassistant/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
|
||||
/tests/components/trafikverket_ferry/ @gjohansson-ST
|
||||
/homeassistant/components/trafikverket_train/ @endor-force @gjohansson-ST
|
||||
@ -1365,6 +1368,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vizio/ @raman325
|
||||
/homeassistant/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
|
||||
/tests/components/voip/ @balloob @synesthesiam
|
||||
/homeassistant/components/volumio/ @OnFreund
|
||||
@ -1375,6 +1380,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vulcan/ @Antoni-Czaplicki
|
||||
/homeassistant/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
|
||||
/tests/components/wallbox/ @hesselonline
|
||||
/homeassistant/components/waqi/ @andrey-git
|
||||
@ -1438,6 +1445,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/yamaha_musiccast/ @vigonotion @micha91
|
||||
/homeassistant/components/yandex_transport/ @rishatik92 @devbis
|
||||
/tests/components/yandex_transport/ @rishatik92 @devbis
|
||||
/homeassistant/components/yardian/ @h3l1o5
|
||||
/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||
/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||
/homeassistant/components/yeelightsunflower/ @lindsaymarkward
|
||||
|
10
build.yaml
10
build.yaml
@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.07.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.07.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.07.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.07.0
|
||||
i386: ghcr.io/home-assistant/i386-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.08.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.08.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.08.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.08.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@ -148,16 +148,6 @@ def get_arguments() -> argparse.Namespace:
|
||||
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:
|
||||
"""Check if there are any lingering threads."""
|
||||
try:
|
||||
|
@ -8,7 +8,7 @@ from typing import Any, Generic, Self, TypeVar, overload
|
||||
_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.
|
||||
|
||||
Includes https://github.com/python/cpython/pull/101890/files
|
||||
|
@ -110,8 +110,7 @@ async def async_setup_hass(
|
||||
runtime_config: RuntimeConfig,
|
||||
) -> core.HomeAssistant | None:
|
||||
"""Set up Home Assistant."""
|
||||
hass = core.HomeAssistant()
|
||||
hass.config.config_dir = runtime_config.config_dir
|
||||
hass = core.HomeAssistant(runtime_config.config_dir)
|
||||
|
||||
async_enable_logging(
|
||||
hass,
|
||||
@ -134,6 +133,7 @@ async def async_setup_hass(
|
||||
|
||||
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
|
||||
|
||||
loader.async_setup(hass)
|
||||
config_dict = None
|
||||
basic_setup_success = False
|
||||
|
||||
@ -177,14 +177,15 @@ async def async_setup_hass(
|
||||
old_config = hass.config
|
||||
old_logging = hass.data.get(DATA_LOGGING)
|
||||
|
||||
hass = core.HomeAssistant()
|
||||
hass = core.HomeAssistant(old_config.config_dir)
|
||||
if old_logging:
|
||||
hass.data[DATA_LOGGING] = old_logging
|
||||
hass.config.skip_pip = old_config.skip_pip
|
||||
hass.config.skip_pip_packages = old_config.skip_pip_packages
|
||||
hass.config.internal_url = old_config.internal_url
|
||||
hass.config.external_url = old_config.external_url
|
||||
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:
|
||||
_LOGGER.info("Starting in safe mode")
|
||||
|
@ -2,6 +2,7 @@
|
||||
"domain": "trafikverket",
|
||||
"name": "Trafikverket",
|
||||
"integrations": [
|
||||
"trafikverket_camera",
|
||||
"trafikverket_ferry",
|
||||
"trafikverket_train",
|
||||
"trafikverket_weatherstation"
|
||||
|
@ -28,6 +28,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
|
||||
@ -287,14 +288,14 @@ class AbodeDevice(AbodeEntity):
|
||||
"""Initialize Abode device."""
|
||||
super().__init__(data)
|
||||
self._device = device
|
||||
self._attr_unique_id = device.device_uuid
|
||||
self._attr_unique_id = device.uuid
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to device events."""
|
||||
await super().async_added_to_hass()
|
||||
await self.hass.async_add_executor_job(
|
||||
self._data.abode.events.add_device_callback,
|
||||
self._device.device_id,
|
||||
self._device.id,
|
||||
self._update_callback,
|
||||
)
|
||||
|
||||
@ -302,7 +303,7 @@ class AbodeDevice(AbodeEntity):
|
||||
"""Unsubscribe from device events."""
|
||||
await super().async_will_remove_from_hass()
|
||||
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:
|
||||
@ -313,17 +314,17 @@ class AbodeDevice(AbodeEntity):
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
"device_id": self._device.device_id,
|
||||
"device_id": self._device.id,
|
||||
"battery_low": self._device.battery_low,
|
||||
"no_response": self._device.no_response,
|
||||
"device_type": self._device.type,
|
||||
}
|
||||
|
||||
@property
|
||||
def device_info(self) -> entity.DeviceInfo:
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return entity.DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device.device_id)},
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device.id)},
|
||||
manufacturer="Abode",
|
||||
model=self._device.type,
|
||||
name=self._device.name,
|
||||
|
@ -69,7 +69,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
"device_id": self._device.device_id,
|
||||
"device_id": self._device.id,
|
||||
"battery_backup": self._device.battery,
|
||||
"cellular_backup": self._device.is_cellular,
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ async def async_setup_entry(
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Support for Abode Security System sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from jaraco.abode.devices.sensor import Sensor as AbodeSense
|
||||
@ -12,25 +14,52 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
)
|
||||
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.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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):
|
||||
"""A sensor implementation for Abode devices."""
|
||||
|
||||
entity_description: AbodeSensorDescription
|
||||
_device: AbodeSense
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: AbodeSystem,
|
||||
device: AbodeSense,
|
||||
description: SensorEntityDescription,
|
||||
description: AbodeSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize a sensor for an Abode device."""
|
||||
super().__init__(data, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.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
|
||||
self._attr_unique_id = f"{device.uuid}-{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
if self.entity_description.key == CONST.TEMP_STATUS_KEY:
|
||||
return cast(float, self._device.temp)
|
||||
if self.entity_description.key == CONST.HUMI_STATUS_KEY:
|
||||
return cast(float, self._device.humidity)
|
||||
if self.entity_description.key == CONST.LUX_STATUS_KEY:
|
||||
return cast(float, self._device.lux)
|
||||
return None
|
||||
return self.entity_description.value_fn(self._device)
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str:
|
||||
"""Return the native unit of measurement."""
|
||||
return self.entity_description.native_unit_of_measurement_fn(self._device)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""The AccuWeather component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
@ -8,7 +9,6 @@ from typing import Any
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
|
||||
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.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER
|
||||
|
@ -2,12 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from typing import Any
|
||||
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
@ -50,3 +50,8 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = {
|
||||
ATTR_CONDITION_SUNNY: [1, 2, 5],
|
||||
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
|
||||
}
|
||||
|
@ -59,193 +59,280 @@ class AccuWeatherSensorDescription(
|
||||
"""Class describing AccuWeather sensor entities."""
|
||||
|
||||
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
|
||||
day: int | None = None
|
||||
|
||||
|
||||
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
AccuWeatherSensorDescription(
|
||||
key="AirQuality",
|
||||
icon="mdi:air-filter",
|
||||
name="Air quality",
|
||||
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
|
||||
translation_key="air_quality",
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="AirQuality",
|
||||
icon="mdi:air-filter",
|
||||
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
|
||||
translation_key=f"air_quality_{day}d",
|
||||
day=day,
|
||||
)
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="CloudCoverDay",
|
||||
icon="mdi:weather-cloudy",
|
||||
name="Cloud cover day",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="CloudCoverDay",
|
||||
icon="mdi:weather-cloudy",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
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(
|
||||
key="CloudCoverNight",
|
||||
icon="mdi:weather-cloudy",
|
||||
name="Cloud cover night",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="CloudCoverNight",
|
||||
icon="mdi:weather-cloudy",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
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(
|
||||
key="Grass",
|
||||
icon="mdi:grass",
|
||||
name="Grass pollen",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key="grass_pollen",
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Grass",
|
||||
icon="mdi:grass",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key=f"grass_pollen_{day}d",
|
||||
day=day,
|
||||
)
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="HoursOfSun",
|
||||
icon="mdi:weather-partly-cloudy",
|
||||
name="Hours of sun",
|
||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||
value_fn=lambda data: cast(float, data),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="HoursOfSun",
|
||||
icon="mdi:weather-partly-cloudy",
|
||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||
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(
|
||||
key="LongPhraseDay",
|
||||
name="Condition day",
|
||||
value_fn=lambda data: cast(str, data),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="LongPhraseDay",
|
||||
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(
|
||||
key="LongPhraseNight",
|
||||
name="Condition night",
|
||||
value_fn=lambda data: cast(str, data),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="LongPhraseNight",
|
||||
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(
|
||||
key="Mold",
|
||||
icon="mdi:blur",
|
||||
name="Mold pollen",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key="mold_pollen",
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Mold",
|
||||
icon="mdi:blur",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key=f"mold_pollen_{day}d",
|
||||
day=day,
|
||||
)
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Ragweed",
|
||||
icon="mdi:sprout",
|
||||
name="Ragweed pollen",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key="ragweed_pollen",
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Ragweed",
|
||||
icon="mdi:sprout",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key=f"ragweed_pollen_{day}d",
|
||||
day=day,
|
||||
)
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureMax",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="RealFeel temperature max",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureMax",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
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(
|
||||
key="RealFeelTemperatureMin",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="RealFeel temperature min",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureMin",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
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(
|
||||
key="RealFeelTemperatureShadeMax",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="RealFeel temperature shade max",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureShadeMax",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
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(
|
||||
key="RealFeelTemperatureShadeMin",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="RealFeel temperature shade min",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureShadeMin",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
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(
|
||||
key="SolarIrradianceDay",
|
||||
icon="mdi:weather-sunny",
|
||||
name="Solar irradiance day",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="SolarIrradianceDay",
|
||||
icon="mdi:weather-sunny",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
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(
|
||||
key="SolarIrradianceNight",
|
||||
icon="mdi:weather-sunny",
|
||||
name="Solar irradiance night",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="SolarIrradianceNight",
|
||||
icon="mdi:weather-sunny",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
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(
|
||||
key="ThunderstormProbabilityDay",
|
||||
icon="mdi:weather-lightning",
|
||||
name="Thunderstorm probability day",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="ThunderstormProbabilityDay",
|
||||
icon="mdi:weather-lightning",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
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(
|
||||
key="ThunderstormProbabilityNight",
|
||||
icon="mdi:weather-lightning",
|
||||
name="Thunderstorm probability night",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="ThunderstormProbabilityNight",
|
||||
icon="mdi:weather-lightning",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
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(
|
||||
key="Tree",
|
||||
icon="mdi:tree-outline",
|
||||
name="Tree pollen",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key="tree_pollen",
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Tree",
|
||||
icon="mdi:tree-outline",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key=f"tree_pollen_{day}d",
|
||||
day=day,
|
||||
)
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="UVIndex",
|
||||
icon="mdi:weather-sunny",
|
||||
name="UV index",
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key="uv_index",
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="UVIndex",
|
||||
icon="mdi:weather-sunny",
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||
translation_key=f"uv_index_{day}d",
|
||||
day=day,
|
||||
)
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindGustDay",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
name="Wind gust day",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindGustDay",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
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(
|
||||
key="WindGustNight",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
name="Wind gust night",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindGustNight",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
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(
|
||||
key="WindDay",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
name="Wind day",
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindDay",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
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(
|
||||
key="WindNight",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
name="Wind night",
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||
*(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindNight",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||
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(
|
||||
key="ApparentTemperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="Apparent temperature",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="apparent_temperature",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Ceiling",
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
icon="mdi:weather-fog",
|
||||
name="Cloud ceiling",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfLength.METERS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
suggested_display_precision=0,
|
||||
translation_key="cloud_ceiling",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="CloudCover",
|
||||
icon="mdi:weather-cloudy",
|
||||
name="Cloud cover",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
translation_key="cloud_cover",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="DewPoint",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="Dew point",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="dew_point",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="RealFeel temperature",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="realfeel_temperature",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureShade",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="RealFeel temperature shade",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="realfeel_temperature_shade",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Precipitation",
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
name="Precipitation",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
attr_fn=lambda data: {"type": data["PrecipitationType"]},
|
||||
translation_key="precipitation",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="PressureTendency",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
icon="mdi:gauge",
|
||||
name="Pressure tendency",
|
||||
options=["falling", "rising", "steady"],
|
||||
translation_key="pressure_tendency",
|
||||
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
|
||||
translation_key="pressure_tendency",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="UVIndex",
|
||||
icon="mdi:weather-sunny",
|
||||
name="UV index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
value_fn=lambda data: cast(int, data),
|
||||
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
|
||||
translation_key="uv_index",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="WetBulbTemperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="Wet bulb temperature",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="wet_bulb_temperature",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindChillTemperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
name="Wind chill temperature",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||
translation_key="wind_chill_temperature",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Wind",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
name="Wind",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
|
||||
translation_key="wind_speed",
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindGust",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
name="Wind gust",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
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:
|
||||
# Some air quality/allergy sensors are only available for certain
|
||||
# locations.
|
||||
sensors.extend(
|
||||
AccuWeatherSensor(coordinator, description, forecast_day=day)
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
for description in FORECAST_SENSOR_TYPES
|
||||
if description.key in coordinator.data[ATTR_FORECAST][0]
|
||||
)
|
||||
for description in FORECAST_SENSOR_TYPES:
|
||||
# Some air quality/allergy sensors are only available for certain
|
||||
# locations.
|
||||
if description.key not in coordinator.data[ATTR_FORECAST][description.day]:
|
||||
continue
|
||||
sensors.append(AccuWeatherSensor(coordinator, description))
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
@ -406,25 +490,21 @@ class AccuWeatherSensor(
|
||||
self,
|
||||
coordinator: AccuWeatherDataUpdateCoordinator,
|
||||
description: AccuWeatherSensorDescription,
|
||||
forecast_day: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self.forecast_day = description.day
|
||||
self.entity_description = description
|
||||
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:
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.location_key}-{description.key}".lower()
|
||||
)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self.forecast_day = forecast_day
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int | float | None:
|
||||
|
@ -24,14 +24,8 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"pressure_tendency": {
|
||||
"state": {
|
||||
"steady": "Steady",
|
||||
"rising": "Rising",
|
||||
"falling": "Falling"
|
||||
}
|
||||
},
|
||||
"air_quality": {
|
||||
"air_quality_0d": {
|
||||
"name": "Air quality today",
|
||||
"state": {
|
||||
"good": "Good",
|
||||
"hazardous": "Hazardous",
|
||||
@ -41,80 +35,761 @@
|
||||
"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": {
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"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": {
|
||||
"grass_pollen_1d": {
|
||||
"name": "Grass pollen day 1",
|
||||
"state_attributes": {
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"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": {
|
||||
"grass_pollen_2d": {
|
||||
"name": "Grass pollen day 2",
|
||||
"state_attributes": {
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"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": {
|
||||
"grass_pollen_3d": {
|
||||
"name": "Grass pollen day 3",
|
||||
"state_attributes": {
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"name": "UV index",
|
||||
"state_attributes": {
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -17,7 +17,8 @@ from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_UV_INDEX,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
SingleCoordinatorWeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@ -27,9 +28,8 @@ from homeassistant.const import (
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from . import AccuWeatherDataUpdateCoordinator
|
||||
@ -40,7 +40,7 @@ from .const import (
|
||||
ATTR_SPEED,
|
||||
ATTR_VALUE,
|
||||
ATTRIBUTION,
|
||||
CONDITION_CLASSES,
|
||||
CONDITION_MAP,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@ -58,7 +58,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class AccuWeatherEntity(
|
||||
CoordinatorEntity[AccuWeatherDataUpdateCoordinator], WeatherEntity
|
||||
SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator]
|
||||
):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
||||
@ -68,9 +68,6 @@ class AccuWeatherEntity(
|
||||
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
|
||||
"""Initialize."""
|
||||
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_pressure_unit = UnitOfPressure.HPA
|
||||
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
@ -79,18 +76,13 @@ class AccuWeatherEntity(
|
||||
self._attr_unique_id = coordinator.location_key
|
||||
self._attr_attribution = ATTRIBUTION
|
||||
self._attr_device_info = coordinator.device_info
|
||||
if self.coordinator.forecast:
|
||||
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
try:
|
||||
return [
|
||||
k
|
||||
for k, v in CONDITION_CLASSES.items()
|
||||
if self.coordinator.data["WeatherIcon"] in v
|
||||
][0]
|
||||
except IndexError:
|
||||
return None
|
||||
return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"])
|
||||
|
||||
@property
|
||||
def cloud_coverage(self) -> float:
|
||||
@ -180,9 +172,12 @@ class AccuWeatherEntity(
|
||||
],
|
||||
ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE],
|
||||
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
|
||||
ATTR_FORECAST_CONDITION: [
|
||||
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
|
||||
][0],
|
||||
ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]),
|
||||
}
|
||||
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
|
||||
|
@ -74,9 +74,9 @@ class AcmedaBase(entity.Entity):
|
||||
return self.roller.id
|
||||
|
||||
@property
|
||||
def device_info(self) -> entity.DeviceInfo:
|
||||
def device_info(self) -> dr.DeviceInfo:
|
||||
"""Return the device info."""
|
||||
return entity.DeviceInfo(
|
||||
return dr.DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
manufacturer="Rollease Acmeda",
|
||||
name=self.roller.name,
|
||||
|
@ -2,11 +2,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
import aiopulse
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -43,7 +43,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
hubs: list[aiopulse.Hub] = []
|
||||
with suppress(asyncio.TimeoutError):
|
||||
async with async_timeout.timeout(5):
|
||||
async with timeout(5):
|
||||
async for hub in aiopulse.Hub.discover():
|
||||
if hub.id not in already_configured:
|
||||
hubs.append(hub)
|
||||
|
@ -23,7 +23,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
|
@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
from adguardhome import AdGuardHome, AdGuardHomeError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
"""Support for Automation Device Specification (ADS)."""
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from collections import namedtuple
|
||||
import ctypes
|
||||
import logging
|
||||
import struct
|
||||
import threading
|
||||
|
||||
import async_timeout
|
||||
import pyads
|
||||
import voluptuous as vol
|
||||
|
||||
@ -301,7 +301,7 @@ class AdsEntity(Entity):
|
||||
self._ads_hub.add_device_notification, ads_var, plctype, update
|
||||
)
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
await self._event.wait()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)
|
||||
|
@ -4,7 +4,7 @@ from typing import Any
|
||||
from advantage_air import ApiError
|
||||
|
||||
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 .const import DOMAIN
|
||||
|
@ -4,7 +4,7 @@ from typing import Any
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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 .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
|
@ -3,7 +3,7 @@
|
||||
from homeassistant.components.update import UpdateEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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 .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
|
@ -1,11 +1,14 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
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.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import (
|
||||
CONF_STATION_UPDATES,
|
||||
@ -27,11 +30,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
longitude = entry.data[CONF_LONGITUDE]
|
||||
station_updates = entry.options.get(CONF_STATION_UPDATES, True)
|
||||
|
||||
aemet = AEMET(api_key)
|
||||
weather_coordinator = WeatherUpdateCoordinator(
|
||||
hass, aemet, latitude, longitude, station_updates
|
||||
)
|
||||
options = ConnectionOptions(api_key, station_updates)
|
||||
aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
|
||||
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()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""Config flow for AEMET OpenData."""
|
||||
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
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
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 (
|
||||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
@ -39,8 +40,11 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(f"{latitude}-{longitude}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY])
|
||||
if not api_online:
|
||||
options = ConnectionOptions(user_input[CONF_API_KEY], False)
|
||||
aemet = AEMET(aiohttp_client.async_get_clientsession(self.hass), options)
|
||||
try:
|
||||
await aemet.select_coordinates(latitude, longitude)
|
||||
except AuthError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
|
||||
if not errors:
|
||||
@ -70,10 +74,3 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
) -> SchemaOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
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
|
||||
)
|
||||
|
@ -33,6 +33,7 @@ ATTR_API_FORECAST_TEMP = "temperature"
|
||||
ATTR_API_FORECAST_TEMP_LOW = "templow"
|
||||
ATTR_API_FORECAST_TIME = "datetime"
|
||||
ATTR_API_FORECAST_WIND_BEARING = "wind_bearing"
|
||||
ATTR_API_FORECAST_WIND_MAX_SPEED = "wind_max_speed"
|
||||
ATTR_API_FORECAST_WIND_SPEED = "wind_speed"
|
||||
ATTR_API_HUMIDITY = "humidity"
|
||||
ATTR_API_PRESSURE = "pressure"
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.2.2"]
|
||||
"requirements": ["AEMET-OpenData==0.4.4"]
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||
ATTR_FORECAST_NATIVE_TEMP,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
WeatherEntity,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
Forecast,
|
||||
SingleCoordinatorWeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@ -17,9 +23,9 @@ from homeassistant.const import (
|
||||
UnitOfSpeed,
|
||||
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.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
@ -30,6 +36,7 @@ from .const import (
|
||||
ATTR_API_FORECAST_TEMP_LOW,
|
||||
ATTR_API_FORECAST_TIME,
|
||||
ATTR_API_FORECAST_WIND_BEARING,
|
||||
ATTR_API_FORECAST_WIND_MAX_SPEED,
|
||||
ATTR_API_FORECAST_WIND_SPEED,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
@ -64,6 +71,7 @@ FORECAST_MAP = {
|
||||
ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP,
|
||||
ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME,
|
||||
ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_API_FORECAST_WIND_MAX_SPEED: ATTR_FORECAST_NATIVE_WIND_GUST_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]
|
||||
|
||||
entities = []
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
unique_id = f"{config_entry.unique_id} {mode}"
|
||||
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
|
||||
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:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
unique_id = f"{config_entry.unique_id} {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)
|
||||
|
||||
|
||||
class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
|
||||
class AemetWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
|
||||
"""Implementation of an AEMET OpenData sensor."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
@ -95,6 +121,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
|
||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -117,15 +146,32 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
|
||||
"""Return the current condition."""
|
||||
return self.coordinator.data[ATTR_API_CONDITION]
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
def _forecast(self, forecast_mode: str) -> list[Forecast]:
|
||||
"""Return the forecast array."""
|
||||
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
|
||||
forecast_map = FORECAST_MAP[self._forecast_mode]
|
||||
return [
|
||||
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
|
||||
for forecast in forecasts
|
||||
]
|
||||
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[forecast_mode]]
|
||||
forecast_map = FORECAST_MAP[forecast_mode]
|
||||
return cast(
|
||||
list[Forecast],
|
||||
[
|
||||
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
|
||||
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
|
||||
def humidity(self):
|
||||
|
@ -1,22 +1,21 @@
|
||||
"""Weather data coordinator for the AEMET OpenData service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
|
||||
from aemet_opendata.const import (
|
||||
AEMET_ATTR_DATE,
|
||||
AEMET_ATTR_DAY,
|
||||
AEMET_ATTR_DIRECTION,
|
||||
AEMET_ATTR_ELABORATED,
|
||||
AEMET_ATTR_FEEL_TEMPERATURE,
|
||||
AEMET_ATTR_FORECAST,
|
||||
AEMET_ATTR_HUMIDITY,
|
||||
AEMET_ATTR_ID,
|
||||
AEMET_ATTR_IDEMA,
|
||||
AEMET_ATTR_MAX,
|
||||
AEMET_ATTR_MIN,
|
||||
AEMET_ATTR_NAME,
|
||||
AEMET_ATTR_PRECIPITATION,
|
||||
AEMET_ATTR_PRECIPITATION_PROBABILITY,
|
||||
AEMET_ATTR_SKY_STATE,
|
||||
@ -25,24 +24,24 @@ from aemet_opendata.const import (
|
||||
AEMET_ATTR_SPEED,
|
||||
AEMET_ATTR_STATION_DATE,
|
||||
AEMET_ATTR_STATION_HUMIDITY,
|
||||
AEMET_ATTR_STATION_LOCATION,
|
||||
AEMET_ATTR_STATION_PRESSURE,
|
||||
AEMET_ATTR_STATION_PRESSURE_SEA,
|
||||
AEMET_ATTR_STATION_TEMPERATURE,
|
||||
AEMET_ATTR_STORM_PROBABILITY,
|
||||
AEMET_ATTR_TEMPERATURE,
|
||||
AEMET_ATTR_TEMPERATURE_FEELING,
|
||||
AEMET_ATTR_WIND,
|
||||
AEMET_ATTR_WIND_GUST,
|
||||
ATTR_DATA,
|
||||
)
|
||||
from aemet_opendata.exceptions import AemetError
|
||||
from aemet_opendata.helpers import (
|
||||
get_forecast_day_value,
|
||||
get_forecast_hour_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.util import dt as dt_util
|
||||
|
||||
@ -57,6 +56,7 @@ from .const import (
|
||||
ATTR_API_FORECAST_TEMP_LOW,
|
||||
ATTR_API_FORECAST_TIME,
|
||||
ATTR_API_FORECAST_WIND_BEARING,
|
||||
ATTR_API_FORECAST_WIND_MAX_SPEED,
|
||||
ATTR_API_FORECAST_WIND_SPEED,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
@ -83,6 +83,7 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_TIMEOUT: Final[int] = 120
|
||||
STATION_MAX_DELTA = timedelta(hours=2)
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
@ -112,128 +113,33 @@ def format_int(value) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
class TownNotFound(UpdateFailed):
|
||||
"""Raised when town is not found."""
|
||||
|
||||
|
||||
class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Weather data update coordinator."""
|
||||
|
||||
def __init__(self, hass, aemet, latitude, longitude, station_updates):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
aemet: AEMET,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
self.aemet = aemet
|
||||
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=WEATHER_UPDATE_INTERVAL,
|
||||
)
|
||||
|
||||
self._aemet = aemet
|
||||
self._station = None
|
||||
self._town = None
|
||||
self._latitude = latitude
|
||||
self._longitude = longitude
|
||||
self._station_updates = station_updates
|
||||
self._data = {
|
||||
"daily": None,
|
||||
"hourly": None,
|
||||
"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"],
|
||||
)
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update coordinator data."""
|
||||
async with timeout(API_TIMEOUT):
|
||||
try:
|
||||
await self.aemet.update()
|
||||
except AemetError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
weather_response = self.aemet.legacy_weather()
|
||||
return self._convert_weather_response(weather_response)
|
||||
|
||||
def _convert_weather_response(self, weather_response):
|
||||
"""Format the weather response correctly."""
|
||||
@ -428,6 +334,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
),
|
||||
ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour),
|
||||
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_BEARING: self._get_wind_bearing(day, hour),
|
||||
}
|
||||
@ -518,14 +425,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
def _get_station_id(self):
|
||||
"""Get station ID from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_IDEMA]
|
||||
if self.aemet.station:
|
||||
return self.aemet.station.get_id()
|
||||
return None
|
||||
|
||||
def _get_station_name(self):
|
||||
"""Get station name from weather data."""
|
||||
if self._station:
|
||||
return self._station[AEMET_ATTR_STATION_LOCATION]
|
||||
if self.aemet.station:
|
||||
return self.aemet.station.get_name()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@ -561,19 +468,19 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
@staticmethod
|
||||
def _get_temperature_feeling(day_data, hour):
|
||||
"""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)
|
||||
|
||||
def _get_town_id(self):
|
||||
"""Get town ID from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_ID]
|
||||
if self.aemet.town:
|
||||
return self.aemet.town.get_id()
|
||||
return None
|
||||
|
||||
def _get_town_name(self):
|
||||
"""Get town name from weather data."""
|
||||
if self._town:
|
||||
return self._town[AEMET_ATTR_NAME]
|
||||
if self.aemet.town:
|
||||
return self.aemet.town.get_name()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@ -623,12 +530,3 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
if val:
|
||||
return format_int(val)
|
||||
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)
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
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 .const import CONNECTION, DOMAIN as AGENT_DOMAIN
|
||||
|
@ -8,7 +8,7 @@ from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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,
|
||||
async_get_current_platform,
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""The Airly integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from math import ceil
|
||||
@ -9,7 +10,6 @@ from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -167,7 +167,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
measurements = self.airly.create_measurements_session_point(
|
||||
self.latitude, self.longitude
|
||||
)
|
||||
async with async_timeout.timeout(20):
|
||||
async with timeout(20):
|
||||
try:
|
||||
await measurements.update()
|
||||
except (AirlyError, ClientConnectorError) as error:
|
||||
|
@ -1,13 +1,13 @@
|
||||
"""Adds config flow for Airly."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -105,7 +105,7 @@ async def test_location(
|
||||
measurements = airly.create_measurements_session_point(
|
||||
latitude=latitude, longitude=longitude
|
||||
)
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
await measurements.update()
|
||||
|
||||
current = measurements.current
|
||||
|
@ -20,8 +20,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
|
@ -47,7 +47,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
latitude = entry.data[CONF_LATITUDE]
|
||||
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
|
||||
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[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)
|
||||
|
||||
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:
|
||||
"""Unload a config entry."""
|
||||
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
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Define an object to hold Airly data."""
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""Config flow for AirNow integration."""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyairnow import WebServiceAPI
|
||||
from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
|
||||
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.helpers.aiohttp_client import async_get_clientsession
|
||||
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):
|
||||
"""Handle a config flow for AirNow."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
@ -75,12 +76,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
# Create Entry
|
||||
radius = user_input.pop(CONF_RADIUS)
|
||||
return self.async_create_entry(
|
||||
title=(
|
||||
f"AirNow Sensor at {user_input[CONF_LATITUDE]},"
|
||||
f" {user_input[CONF_LONGITUDE]}"
|
||||
),
|
||||
data=user_input,
|
||||
options={CONF_RADIUS: radius},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
@ -94,12 +97,49 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Optional(
|
||||
CONF_LONGITUDE, default=self.hass.config.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,
|
||||
)
|
||||
|
||||
@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):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
@ -17,8 +17,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@ -30,6 +29,9 @@ from .const import (
|
||||
ATTR_API_AQI_LEVEL,
|
||||
ATTR_API_O3,
|
||||
ATTR_API_PM25,
|
||||
ATTR_API_STATION,
|
||||
ATTR_API_STATION_LATITUDE,
|
||||
ATTR_API_STATION_LONGITUDE,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
@ -40,6 +42,7 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
ATTR_DESCR = "description"
|
||||
ATTR_LEVEL = "level"
|
||||
ATTR_STATION = "reporting_station"
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -85,6 +88,16 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
||||
value_fn=lambda data: data.get(ATTR_API_O3),
|
||||
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],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -21,10 +21,26 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"radius": "Station Radius (miles)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"o3": {
|
||||
"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%]" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
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.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
|
@ -34,8 +34,12 @@ class Discovery:
|
||||
|
||||
|
||||
def get_name(device: AirthingsDevice) -> str:
|
||||
"""Generate name with identifier for device."""
|
||||
return f"{device.name} ({device.identifier})"
|
||||
"""Generate name with model and identifier for device."""
|
||||
|
||||
name = device.friendly_name()
|
||||
if identifier := device.identifier:
|
||||
name += f" ({identifier})"
|
||||
return name
|
||||
|
||||
|
||||
class AirthingsDeviceUpdateError(Exception):
|
||||
@ -156,7 +160,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
titles = {
|
||||
address: get_name(discovery.device)
|
||||
address: discovery.device.name
|
||||
for (address, discovery) in self._discovered_devices.items()
|
||||
}
|
||||
return self.async_show_form(
|
||||
|
@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==0.5.3"]
|
||||
"requirements": ["airthings-ble==0.5.6-2"]
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
@ -162,11 +161,11 @@ class AirthingsSensor(
|
||||
super().__init__(coordinator)
|
||||
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._id = airthings_device.address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={
|
||||
(
|
||||
@ -175,9 +174,10 @@ class AirthingsSensor(
|
||||
)
|
||||
},
|
||||
name=name,
|
||||
manufacturer="Airthings",
|
||||
manufacturer=airthings_device.manufacturer,
|
||||
hw_version=airthings_device.hw_version,
|
||||
sw_version=airthings_device.sw_version,
|
||||
model=airthings_device.model,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
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.update_coordinator import CoordinatorEntity
|
||||
|
||||
@ -98,28 +98,20 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
||||
self._ac_number = ac_number
|
||||
self._airtouch = coordinator.airtouch
|
||||
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
|
||||
def _handle_coordinator_update(self):
|
||||
self._unit = self._airtouch.GetAcs()[self._ac_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)},
|
||||
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
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
@ -208,29 +200,21 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
||||
"""Initialize the climate device."""
|
||||
super().__init__(coordinator)
|
||||
self._group_number = group_number
|
||||
self._attr_unique_id = group_number
|
||||
self._airtouch = coordinator.airtouch
|
||||
self._info = info
|
||||
self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number)
|
||||
|
||||
@callback
|
||||
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)},
|
||||
self._unit = self._airtouch.GetGroupByGroupNumber(group_number)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, group_number)},
|
||||
manufacturer="Airtouch",
|
||||
model="Airtouch 4",
|
||||
name=self._unit.GroupName,
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID for this device."""
|
||||
return self._group_number
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number)
|
||||
return super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "airtouch4",
|
||||
"name": "AirTouch 4",
|
||||
"codeowners": ["@LonePurpleWolf"],
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
||||
"iot_class": "local_polling",
|
||||
|
@ -109,19 +109,21 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"airvisual_checked_api_keys_lock", asyncio.Lock()
|
||||
)
|
||||
|
||||
if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
|
||||
coro = cloud_api.air_quality.nearest_city()
|
||||
error_schema = self.geography_coords_schema
|
||||
error_step = "geography_by_coords"
|
||||
else:
|
||||
coro = cloud_api.air_quality.city(
|
||||
user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY]
|
||||
)
|
||||
error_schema = GEOGRAPHY_NAME_SCHEMA
|
||||
error_step = "geography_by_name"
|
||||
|
||||
async with valid_keys_lock:
|
||||
if user_input[CONF_API_KEY] not in valid_keys:
|
||||
if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
|
||||
coro = cloud_api.air_quality.nearest_city()
|
||||
error_schema = self.geography_coords_schema
|
||||
error_step = "geography_by_coords"
|
||||
else:
|
||||
coro = cloud_api.air_quality.city(
|
||||
user_input[CONF_CITY],
|
||||
user_input[CONF_STATE],
|
||||
user_input[CONF_COUNTRY],
|
||||
)
|
||||
error_schema = GEOGRAPHY_NAME_SCHEMA
|
||||
error_step = "geography_by_name"
|
||||
|
||||
try:
|
||||
await coro
|
||||
except (InvalidKeyError, KeyExpiredError, UnauthorizedError):
|
||||
|
@ -23,7 +23,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
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 (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
|
@ -1,13 +1,13 @@
|
||||
"""The Airzone integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone.exceptions import AirzoneError
|
||||
from aioairzone.localapi import AirzoneLocalApi
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
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]:
|
||||
"""Update data via library."""
|
||||
async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
|
||||
async with timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
|
||||
try:
|
||||
await self.airzone.update()
|
||||
except AirzoneError as error:
|
||||
|
@ -10,6 +10,7 @@ from aioairzone.const import (
|
||||
AZD_AVAILABLE,
|
||||
AZD_FIRMWARE,
|
||||
AZD_FULL_NAME,
|
||||
AZD_HOT_WATER,
|
||||
AZD_ID,
|
||||
AZD_MAC,
|
||||
AZD_MODEL,
|
||||
@ -26,7 +27,7 @@ from aioairzone.exceptions import AirzoneError
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
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 .const import DOMAIN, MANUFACTURER
|
||||
@ -81,6 +82,31 @@ class AirzoneSystemEntity(AirzoneEntity):
|
||||
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):
|
||||
"""Define an Airzone WebServer entity."""
|
||||
|
||||
|
@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.6.5"]
|
||||
"requirements": ["aioairzone==0.6.8"]
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone.const import (
|
||||
AZD_HOT_WATER,
|
||||
AZD_HUMIDITY,
|
||||
AZD_NAME,
|
||||
AZD_TEMP,
|
||||
@ -31,7 +32,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS
|
||||
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, ...]] = (
|
||||
SensorEntityDescription(
|
||||
@ -71,6 +86,18 @@ async def async_setup_entry(
|
||||
|
||||
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:
|
||||
ws_data = coordinator.data[AZD_WEBSERVER]
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""Define an Airzone WebServer sensor."""
|
||||
|
||||
|
@ -9,6 +9,7 @@ from aioairzone_cloud.const import (
|
||||
AZD_AIDOOS,
|
||||
AZD_ERRORS,
|
||||
AZD_PROBLEMS,
|
||||
AZD_SYSTEMS,
|
||||
AZD_WARNINGS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
@ -25,7 +26,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity
|
||||
from .entity import (
|
||||
AirzoneAidooEntity,
|
||||
AirzoneEntity,
|
||||
AirzoneSystemEntity,
|
||||
AirzoneZoneEntity,
|
||||
)
|
||||
|
||||
|
||||
@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, ...]] = (
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
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 description in ZONE_BINARY_SENSOR_TYPES:
|
||||
if description.key in zone_data:
|
||||
@ -145,6 +177,27 @@ class AirzoneAidooBinarySensor(AirzoneAidooEntity, AirzoneBinarySensor):
|
||||
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):
|
||||
"""Define an Airzone Cloud Zone binary sensor."""
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
"""The Airzone Cloud integration coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
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]:
|
||||
"""Update data via library."""
|
||||
async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
|
||||
async with timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
|
||||
try:
|
||||
await self.airzone.update()
|
||||
except AirzoneCloudError as error:
|
||||
|
@ -10,13 +10,14 @@ from aioairzone_cloud.const import (
|
||||
AZD_FIRMWARE,
|
||||
AZD_NAME,
|
||||
AZD_SYSTEM_ID,
|
||||
AZD_SYSTEMS,
|
||||
AZD_WEBSERVER,
|
||||
AZD_WEBSERVERS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
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 .const import DOMAIN, MANUFACTURER
|
||||
@ -65,6 +66,35 @@ class AirzoneAidooEntity(AirzoneEntity):
|
||||
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):
|
||||
"""Define an Airzone Cloud WebServer entity."""
|
||||
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
|
||||
from homeassistant.core import HomeAssistant
|
||||
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 .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS
|
||||
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 .const import DOMAIN
|
||||
|
@ -16,8 +16,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_point_in_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import (
|
||||
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)
|
||||
except NoDeviceError:
|
||||
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
|
||||
async_track_point_in_time(
|
||||
hass, open_connection, dt_util.utcnow() + timedelta(seconds=5)
|
||||
)
|
||||
async_call_later(hass, timedelta(seconds=5), open_connection)
|
||||
return
|
||||
_LOGGER.debug("Established a connection with the alarmdecoder")
|
||||
hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entityfilter
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import flash_briefings, intent, smart_home_http
|
||||
from . import flash_briefings, intent, smart_home
|
||||
from .const import (
|
||||
CONF_AUDIO,
|
||||
CONF_DISPLAY_CATEGORIES,
|
||||
@ -100,6 +100,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if CONF_SMART_HOME in config:
|
||||
smart_home_config: dict[str, Any] | None = config[CONF_SMART_HOME]
|
||||
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
|
||||
|
@ -1,15 +1,16 @@
|
||||
"""Support for Alexa skill auth."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from asyncio import timeout
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
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.storage import Store
|
||||
from homeassistant.util import dt as dt_util
|
||||
@ -30,24 +31,24 @@ STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||
class Auth:
|
||||
"""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."""
|
||||
self.hass = hass
|
||||
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
self._prefs = None
|
||||
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
self._prefs: dict[str, Any] | None = None
|
||||
self._store: Store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
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."""
|
||||
# access token not retrieved yet for the first time, so this should
|
||||
# be an access token request
|
||||
|
||||
lwa_params = {
|
||||
lwa_params: dict[str, str] = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": accept_grant_code,
|
||||
CONF_CLIENT_ID: self.client_id,
|
||||
@ -61,25 +62,28 @@ class Auth:
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
@callback
|
||||
def async_invalidate_access_token(self):
|
||||
def async_invalidate_access_token(self) -> None:
|
||||
"""Invalidate access token."""
|
||||
assert self._prefs is not 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."""
|
||||
async with self._get_token_lock:
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
|
||||
assert self._prefs is not None
|
||||
if self.is_token_valid():
|
||||
_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:
|
||||
_LOGGER.debug("Token invalid and no refresh token available")
|
||||
return None
|
||||
|
||||
lwa_params = {
|
||||
lwa_params: dict[str, str] = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
|
||||
CONF_CLIENT_ID: self.client_id,
|
||||
@ -90,22 +94,26 @@ class Auth:
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
@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."""
|
||||
assert self._prefs is not None
|
||||
if not self._prefs[STORAGE_ACCESS_TOKEN]:
|
||||
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(
|
||||
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
|
||||
)
|
||||
|
||||
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:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
async with async_timeout.timeout(10):
|
||||
async with timeout(10):
|
||||
response = await session.post(
|
||||
LWA_TOKEN_URI,
|
||||
headers=LWA_HEADERS,
|
||||
@ -127,9 +135,9 @@ class Auth:
|
||||
response_json = await response.json()
|
||||
_LOGGER.debug("LWA response body : %s", response_json)
|
||||
|
||||
access_token = response_json["access_token"]
|
||||
refresh_token = response_json["refresh_token"]
|
||||
expires_in = response_json["expires_in"]
|
||||
access_token: str = response_json["access_token"]
|
||||
refresh_token: str = response_json["refresh_token"]
|
||||
expires_in: int = response_json["expires_in"]
|
||||
expire_time = dt_util.utcnow() + timedelta(seconds=expires_in)
|
||||
|
||||
await self._async_update_preferences(
|
||||
@ -138,7 +146,7 @@ class Auth:
|
||||
|
||||
return access_token
|
||||
|
||||
async def async_load_preferences(self):
|
||||
async def async_load_preferences(self) -> None:
|
||||
"""Load preferences with stored tokens."""
|
||||
self._prefs = await self._store.async_load()
|
||||
|
||||
@ -149,10 +157,13 @@ class Auth:
|
||||
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."""
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
assert self._prefs is not None
|
||||
|
||||
if access_token is not None:
|
||||
self._prefs[STORAGE_ACCESS_TOKEN] = access_token
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,9 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.storage import Store
|
||||
@ -33,38 +36,38 @@ class AbstractConfig(ABC):
|
||||
await self._store.async_load()
|
||||
|
||||
@property
|
||||
def supports_auth(self):
|
||||
def supports_auth(self) -> bool:
|
||||
"""Return if config supports auth."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
def should_report_state(self) -> bool:
|
||||
"""Return if states should be proactively reported."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
@abstractmethod
|
||||
def endpoint(self) -> str | URL | None:
|
||||
"""Endpoint for report state."""
|
||||
return None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def locale(self):
|
||||
def locale(self) -> str | None:
|
||||
"""Return config locale."""
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
def entity_config(self) -> dict[str, Any]:
|
||||
"""Return entity config."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def is_reporting_states(self):
|
||||
def is_reporting_states(self) -> bool:
|
||||
"""Return if proactive mode is enabled."""
|
||||
return self._unsub_proactive_report is not None
|
||||
|
||||
@callback
|
||||
@abstractmethod
|
||||
def user_identifier(self):
|
||||
def user_identifier(self) -> str:
|
||||
"""Return an identifier for the user that represents this config."""
|
||||
|
||||
async def async_enable_proactive_mode(self) -> None:
|
||||
@ -85,29 +88,29 @@ class AbstractConfig(ABC):
|
||||
self._unsub_proactive_report = None
|
||||
|
||||
@callback
|
||||
def should_expose(self, entity_id):
|
||||
def should_expose(self, entity_id: str) -> bool:
|
||||
"""If an entity should be exposed."""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def async_invalidate_access_token(self):
|
||||
def async_invalidate_access_token(self) -> None:
|
||||
"""Invalidate access token."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_access_token(self):
|
||||
async def async_get_access_token(self) -> str | None:
|
||||
"""Get an access token."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_accept_grant(self, code):
|
||||
async def async_accept_grant(self, code: str) -> str | None:
|
||||
"""Accept a grant."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def authorized(self):
|
||||
def authorized(self) -> bool:
|
||||
"""Return authorization status."""
|
||||
return self._store.authorized
|
||||
|
||||
async def set_authorized(self, authorized) -> None:
|
||||
async def set_authorized(self, authorized: bool) -> None:
|
||||
"""Set authorization status.
|
||||
|
||||
- Set when an incoming message is received from Alexa.
|
||||
@ -132,25 +135,26 @@ class AlexaConfigStore:
|
||||
_STORAGE_VERSION = 1
|
||||
_STORAGE_KEY = DOMAIN
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize a configuration store."""
|
||||
self._data = None
|
||||
self._data: dict[str, Any] | None = None
|
||||
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
|
||||
def authorized(self):
|
||||
def authorized(self) -> bool:
|
||||
"""Return authorization status."""
|
||||
return self._data[STORE_AUTHORIZED]
|
||||
assert self._data is not None
|
||||
return bool(self._data[STORE_AUTHORIZED])
|
||||
|
||||
@callback
|
||||
def set_authorized(self, authorized):
|
||||
def set_authorized(self, authorized: bool) -> None:
|
||||
"""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._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."""
|
||||
if data := await self._store.async_load():
|
||||
self._data = data
|
||||
|
@ -69,7 +69,7 @@ API_TEMP_UNITS = {
|
||||
# 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
|
||||
# back to HA state.
|
||||
API_THERMOSTAT_MODES = OrderedDict(
|
||||
API_THERMOSTAT_MODES: OrderedDict[str, str] = OrderedDict(
|
||||
[
|
||||
(climate.HVACMode.HEAT, "HEAT"),
|
||||
(climate.HVACMode.COOL, "COOL"),
|
||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator, Iterable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components import (
|
||||
alarm_control_panel,
|
||||
@ -274,22 +274,23 @@ class AlexaEntity:
|
||||
self.entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
def entity_id(self) -> str:
|
||||
"""Return the Entity ID."""
|
||||
return self.entity.entity_id
|
||||
|
||||
def friendly_name(self):
|
||||
def friendly_name(self) -> str:
|
||||
"""Return the Alexa API friendly name."""
|
||||
return self.entity_conf.get(CONF_NAME, self.entity.name).translate(
|
||||
TRANSLATION_TABLE
|
||||
)
|
||||
friendly_name: str = self.entity_conf.get(
|
||||
CONF_NAME, self.entity.name
|
||||
).translate(TRANSLATION_TABLE)
|
||||
return friendly_name
|
||||
|
||||
def description(self):
|
||||
def description(self) -> str:
|
||||
"""Return the Alexa API description."""
|
||||
description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id
|
||||
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 generate_alexa_id(self.entity.entity_id)
|
||||
|
||||
@ -317,7 +318,7 @@ class AlexaEntity:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def serialize_properties(self):
|
||||
def serialize_properties(self) -> Generator[dict[str, Any], None, None]:
|
||||
"""Yield each supported property in API format."""
|
||||
for interface in self.interfaces():
|
||||
if not interface.properties_proactively_reported():
|
||||
@ -325,9 +326,9 @@ class AlexaEntity:
|
||||
|
||||
yield from interface.serialize_properties()
|
||||
|
||||
def serialize_discovery(self):
|
||||
def serialize_discovery(self) -> dict[str, Any]:
|
||||
"""Serialize the entity for discovery."""
|
||||
result = {
|
||||
result: dict[str, Any] = {
|
||||
"displayCategories": self.display_categories(),
|
||||
"cookie": {},
|
||||
"endpointId": self.alexa_id(),
|
||||
@ -366,7 +367,7 @@ def async_get_entities(
|
||||
hass: HomeAssistant, config: AbstractConfig
|
||||
) -> list[AlexaEntity]:
|
||||
"""Return all entities that are supported by Alexa."""
|
||||
entities = []
|
||||
entities: list[AlexaEntity] = []
|
||||
for state in hass.states.async_all():
|
||||
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
continue
|
||||
@ -725,7 +726,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
class SceneCapabilities(AlexaEntity):
|
||||
"""Class to represent Scene capabilities."""
|
||||
|
||||
def description(self):
|
||||
def description(self) -> str:
|
||||
"""Return the Alexa API description."""
|
||||
description = AlexaEntity.description(self)
|
||||
if "scene" not in description.casefold():
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""Alexa related errors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import API_TEMP_UNITS
|
||||
@ -29,7 +30,9 @@ class AlexaError(Exception):
|
||||
namespace: 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."""
|
||||
Exception.__init__(self)
|
||||
self.error_message = error_message
|
||||
@ -42,7 +45,7 @@ class AlexaInvalidEndpointError(AlexaError):
|
||||
namespace = "Alexa"
|
||||
error_type = "NO_SUCH_ENDPOINT"
|
||||
|
||||
def __init__(self, endpoint_id):
|
||||
def __init__(self, endpoint_id: str) -> None:
|
||||
"""Initialize invalid endpoint error."""
|
||||
msg = f"The endpoint {endpoint_id} does not exist"
|
||||
AlexaError.__init__(self, msg)
|
||||
@ -93,7 +96,9 @@ class AlexaTempRangeError(AlexaError):
|
||||
namespace = "Alexa"
|
||||
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."""
|
||||
unit = hass.config.units.temperature_unit
|
||||
temp_range = {
|
||||
|
@ -4,10 +4,13 @@ from http import HTTPStatus
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from aiohttp.web_response import StreamResponse
|
||||
|
||||
from homeassistant.components import http
|
||||
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.typing import ConfigType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
@ -32,7 +35,7 @@ FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass, flash_briefing_config):
|
||||
def async_setup(hass: HomeAssistant, flash_briefing_config: ConfigType) -> None:
|
||||
"""Activate Alexa component."""
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
|
||||
|
||||
@ -44,14 +47,16 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
requires_auth = False
|
||||
name = "api:alexa:flash_briefings"
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
def __init__(self, hass: HomeAssistant, flash_briefings: ConfigType) -> None:
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__()
|
||||
self.flash_briefings = flash_briefings
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
@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."""
|
||||
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
|
||||
|
||||
|
@ -75,8 +75,7 @@ from .errors import (
|
||||
AlexaUnsupportedThermostatModeError,
|
||||
AlexaVideoActionNotPermittedForContentError,
|
||||
)
|
||||
from .messages import AlexaDirective, AlexaResponse
|
||||
from .state_report import async_enable_proactive_mode
|
||||
from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive"
|
||||
@ -124,7 +123,7 @@ async def async_api_accept_grant(
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
auth_code = directive.payload["grant"]["code"]
|
||||
auth_code: str = directive.payload["grant"]["code"]
|
||||
_LOGGER.debug("AcceptGrant code: %s", auth_code)
|
||||
|
||||
if config.supports_auth:
|
||||
@ -340,8 +339,8 @@ async def async_api_decrease_color_temp(
|
||||
) -> AlexaResponse:
|
||||
"""Process a decrease color temperature request."""
|
||||
entity = directive.entity
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
|
||||
current = int(entity.attributes[light.ATTR_COLOR_TEMP])
|
||||
max_mireds = int(entity.attributes[light.ATTR_MAX_MIREDS])
|
||||
|
||||
value = min(max_mireds, current + 50)
|
||||
await hass.services.async_call(
|
||||
@ -364,8 +363,8 @@ async def async_api_increase_color_temp(
|
||||
) -> AlexaResponse:
|
||||
"""Process an increase color temperature request."""
|
||||
entity = directive.entity
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||
current = int(entity.attributes[light.ATTR_COLOR_TEMP])
|
||||
min_mireds = int(entity.attributes[light.ATTR_MIN_MIREDS])
|
||||
|
||||
value = max(min_mireds, current - 50)
|
||||
await hass.services.async_call(
|
||||
@ -404,7 +403,7 @@ async def async_api_activate(
|
||||
context=context,
|
||||
)
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
"cause": {"type": Cause.VOICE_INTERACTION},
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
}
|
||||
@ -433,7 +432,7 @@ async def async_api_deactivate(
|
||||
context=context,
|
||||
)
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
"cause": {"type": Cause.VOICE_INTERACTION},
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
}
|
||||
@ -475,7 +474,24 @@ async def async_api_unlock(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""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 = (
|
||||
"The unlock directive is not supported for the following locales:"
|
||||
f" {config.locale}"
|
||||
@ -510,7 +526,7 @@ async def async_api_set_volume(
|
||||
volume = round(float(directive.payload["volume"] / 100), 2)
|
||||
entity = directive.entity
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
@ -555,7 +571,7 @@ async def async_api_select_input(
|
||||
)
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_INPUT_SOURCE: media_input,
|
||||
}
|
||||
@ -582,7 +598,7 @@ async def async_api_adjust_volume(
|
||||
volume_delta = int(directive.payload["volume"])
|
||||
|
||||
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
|
||||
try:
|
||||
@ -592,7 +608,7 @@ async def async_api_adjust_volume(
|
||||
|
||||
volume = float(max(0, volume_delta + current) / 100)
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
@ -632,7 +648,7 @@ async def async_api_adjust_volume_step(
|
||||
if is_default:
|
||||
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)):
|
||||
await hass.services.async_call(
|
||||
@ -653,7 +669,7 @@ async def async_api_set_mute(
|
||||
"""Process a set mute request."""
|
||||
mute = bool(directive.payload["mute"])
|
||||
entity = directive.entity
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
@ -674,7 +690,7 @@ async def async_api_play(
|
||||
) -> AlexaResponse:
|
||||
"""Process a play request."""
|
||||
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(
|
||||
entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context
|
||||
@ -692,7 +708,7 @@ async def async_api_pause(
|
||||
) -> AlexaResponse:
|
||||
"""Process a pause request."""
|
||||
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(
|
||||
entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context
|
||||
@ -710,7 +726,7 @@ async def async_api_stop(
|
||||
) -> AlexaResponse:
|
||||
"""Process a stop request."""
|
||||
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(
|
||||
entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context
|
||||
@ -728,7 +744,7 @@ async def async_api_next(
|
||||
) -> AlexaResponse:
|
||||
"""Process a next request."""
|
||||
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(
|
||||
entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context
|
||||
@ -746,7 +762,7 @@ async def async_api_previous(
|
||||
) -> AlexaResponse:
|
||||
"""Process a previous request."""
|
||||
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(
|
||||
entity.domain,
|
||||
@ -759,7 +775,9 @@ async def async_api_previous(
|
||||
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."""
|
||||
to_unit = hass.config.units.temperature_unit
|
||||
from_unit = UnitOfTemperature.CELSIUS
|
||||
@ -785,11 +803,11 @@ async def async_api_set_target_temp(
|
||||
) -> AlexaResponse:
|
||||
"""Process a set target temperature request."""
|
||||
entity = directive.entity
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
|
||||
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
|
||||
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
|
||||
response = directive.response()
|
||||
@ -849,9 +867,10 @@ async def async_api_adjust_target_temp(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process an adjust target temperature request."""
|
||||
data: dict[str, Any]
|
||||
entity = directive.entity
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
|
||||
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
|
||||
unit = hass.config.units.temperature_unit
|
||||
|
||||
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_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
|
||||
if target_temp_high < min_temp or target_temp_high > max_temp:
|
||||
raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp)
|
||||
@ -892,7 +911,7 @@ async def async_api_adjust_target_temp(
|
||||
}
|
||||
)
|
||||
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:
|
||||
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
|
||||
@ -925,11 +944,13 @@ async def async_api_set_thermostat_mode(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process a set thermostat mode request."""
|
||||
operation_list: list[str]
|
||||
|
||||
entity = directive.entity
|
||||
mode = directive.payload["thermostatMode"]
|
||||
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)
|
||||
|
||||
@ -944,7 +965,7 @@ async def async_api_set_thermostat_mode(
|
||||
data[climate.ATTR_PRESET_MODE] = ha_preset
|
||||
|
||||
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 = next(
|
||||
(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
|
||||
|
||||
else:
|
||||
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_mode = next(iter(set(ha_modes).intersection(operation_list)), None)
|
||||
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, [])
|
||||
ha_modes: dict[str, str] = {
|
||||
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:
|
||||
msg = f"The requested thermostat mode {mode} is not supported"
|
||||
raise AlexaUnsupportedThermostatModeError(msg)
|
||||
@ -1007,7 +1032,7 @@ async def async_api_arm(
|
||||
entity = directive.entity
|
||||
service = None
|
||||
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:
|
||||
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
|
||||
payload = {"exitDelayInSeconds": 0}
|
||||
payload: dict[str, Any] = {"exitDelayInSeconds": 0}
|
||||
|
||||
response = directive.response(
|
||||
name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload
|
||||
@ -1053,7 +1078,7 @@ async def async_api_disarm(
|
||||
) -> AlexaResponse:
|
||||
"""Process a Security Panel Disarm request."""
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
response = directive.response()
|
||||
|
||||
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
||||
@ -1095,7 +1120,7 @@ async def async_api_set_mode(
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
mode = directive.payload["mode"]
|
||||
|
||||
# Fan Direction
|
||||
@ -1108,8 +1133,11 @@ async def async_api_set_mode(
|
||||
# Fan preset_mode
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
|
||||
preset_mode = mode.split(".")[1]
|
||||
if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get(
|
||||
fan.ATTR_PRESET_MODES
|
||||
preset_modes: list[str] | None = entity.attributes.get(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
|
||||
data[fan.ATTR_PRESET_MODE] = preset_mode
|
||||
@ -1120,9 +1148,8 @@ async def async_api_set_mode(
|
||||
# Humidifier mode
|
||||
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
|
||||
mode = mode.split(".")[1]
|
||||
if mode != PRESET_MODE_NA and mode in entity.attributes.get(
|
||||
humidifier.ATTR_AVAILABLE_MODES
|
||||
):
|
||||
modes: list[str] | None = entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES)
|
||||
if mode != PRESET_MODE_NA and modes and mode in modes:
|
||||
service = humidifier.SERVICE_SET_MODE
|
||||
data[humidifier.ATTR_MODE] = mode
|
||||
else:
|
||||
@ -1195,7 +1222,7 @@ async def async_api_toggle_on(
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
fan.ATTR_OSCILLATING: True,
|
||||
}
|
||||
@ -1234,7 +1261,7 @@ async def async_api_toggle_off(
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
fan.ATTR_OSCILLATING: False,
|
||||
}
|
||||
@ -1268,7 +1295,7 @@ async def async_api_set_range(
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_value = directive.payload["rangeValue"]
|
||||
|
||||
# Cover Position
|
||||
@ -1537,7 +1564,7 @@ async def async_api_changechannel(
|
||||
channel = metadata_payload["name"]
|
||||
payload_name = "callSign"
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
|
||||
@ -1577,7 +1604,7 @@ async def async_api_skipchannel(
|
||||
channel = int(directive.payload["channelCount"])
|
||||
entity = directive.entity
|
||||
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if channel < 0:
|
||||
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:
|
||||
seek_position = media_duration
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_SEEK_POSITION: seek_position,
|
||||
}
|
||||
@ -1640,7 +1667,9 @@ async def async_api_seek(
|
||||
# convert seconds to milliseconds for StateReport.
|
||||
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(
|
||||
name="StateReport", namespace="Alexa.SeekController", payload=payload
|
||||
)
|
||||
@ -1656,7 +1685,7 @@ async def async_api_set_eq_mode(
|
||||
"""Process a SetMode request for EqualizerController."""
|
||||
mode = directive.payload["mode"]
|
||||
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)
|
||||
if sound_mode_list and mode.lower() in sound_mode_list:
|
||||
@ -1702,7 +1731,7 @@ async def async_api_hold(
|
||||
) -> AlexaResponse:
|
||||
"""Process a TimeHoldController Hold request."""
|
||||
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:
|
||||
service = timer.SERVICE_PAUSE
|
||||
@ -1729,7 +1758,7 @@ async def async_api_resume(
|
||||
) -> AlexaResponse:
|
||||
"""Process a TimeHoldController Resume request."""
|
||||
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:
|
||||
service = timer.SERVICE_START
|
||||
@ -1774,7 +1803,7 @@ async def async_api_initialize_camera_stream(
|
||||
"Failed to find suitable URL to serve to Alexa"
|
||||
) from err
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
"cameraStreams": [
|
||||
{
|
||||
"uri": f"{external_url}{stream_source}",
|
||||
|
@ -3,8 +3,10 @@ import enum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.web import Response
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.util.decorator import Registry
|
||||
@ -18,7 +20,7 @@ HANDLERS = Registry() # type: ignore[var-annotated]
|
||||
INTENTS_API_ENDPOINT = "/api/alexa"
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
class SpeechType(enum.StrEnum):
|
||||
"""The Alexa speech types."""
|
||||
|
||||
plaintext = "PlainText"
|
||||
@ -28,7 +30,7 @@ class SpeechType(enum.Enum):
|
||||
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
class CardType(enum.StrEnum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
simple = "Simple"
|
||||
@ -36,12 +38,12 @@ class CardType(enum.Enum):
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass):
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Activate Alexa component."""
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
|
||||
|
||||
async def async_setup_intents(hass):
|
||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
"""Do intents setup.
|
||||
|
||||
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
|
||||
name = "api:alexa"
|
||||
|
||||
async def post(self, request):
|
||||
async def post(self, request: http.HomeAssistantRequest) -> Response | bytes:
|
||||
"""Handle Alexa."""
|
||||
hass = request.app["hass"]
|
||||
message = await request.json()
|
||||
hass: HomeAssistant = request.app["hass"]
|
||||
message: dict[str, Any] = await request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa request: %s", message)
|
||||
|
||||
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)
|
||||
except UnknownRequest as 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."""
|
||||
alexa_intent_info = message.get("request").get("intent")
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
alexa_intent_info = message["request"].get("intent")
|
||||
alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
|
||||
alexa_response.add_speech(SpeechType.plaintext, error)
|
||||
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.
|
||||
|
||||
Raises:
|
||||
@ -117,19 +123,22 @@ async def async_handle_message(hass, message):
|
||||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get("request")
|
||||
req = message["request"]
|
||||
req_type = req["type"]
|
||||
|
||||
if not (handler := HANDLERS.get(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("IntentRequest")
|
||||
@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.
|
||||
|
||||
Raises:
|
||||
@ -138,9 +147,9 @@ async def async_handle_intent(hass, message):
|
||||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get("request")
|
||||
req = message["request"]
|
||||
alexa_intent_info = req.get("intent")
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
alexa_response = AlexaIntentResponse(hass, alexa_intent_info)
|
||||
|
||||
if req["type"] == "LaunchRequest":
|
||||
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
|
||||
# reference to the request object structure, see the Alexa docs:
|
||||
# https://tinyurl.com/ybvm7jhs
|
||||
resolved_data = {}
|
||||
resolved_data: dict[str, Any] = {}
|
||||
resolved_data["value"] = request["value"]
|
||||
resolved_data["id"] = ""
|
||||
|
||||
@ -226,18 +235,18 @@ def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
|
||||
return resolved_data
|
||||
|
||||
|
||||
class AlexaResponse:
|
||||
class AlexaIntentResponse:
|
||||
"""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."""
|
||||
self.hass = hass
|
||||
self.speech = None
|
||||
self.card = None
|
||||
self.reprompt = None
|
||||
self.session_attributes = {}
|
||||
self.speech: dict[str, Any] | None = None
|
||||
self.card: dict[str, Any] | None = None
|
||||
self.reprompt: dict[str, Any] | None = None
|
||||
self.session_attributes: dict[str, Any] = {}
|
||||
self.should_end_session = True
|
||||
self.variables = {}
|
||||
self.variables: dict[str, Any] = {}
|
||||
|
||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||
if intent_info is not None:
|
||||
@ -252,7 +261,7 @@ class AlexaResponse:
|
||||
self.variables[_key] = _slot_data["value"]
|
||||
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."""
|
||||
assert self.card is None
|
||||
|
||||
@ -266,7 +275,7 @@ class AlexaResponse:
|
||||
card["content"] = content
|
||||
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."""
|
||||
assert self.speech is None
|
||||
|
||||
@ -274,7 +283,7 @@ class AlexaResponse:
|
||||
|
||||
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."""
|
||||
assert self.reprompt is None
|
||||
|
||||
@ -284,9 +293,9 @@ class AlexaResponse:
|
||||
|
||||
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."""
|
||||
response = {"shouldEndSession": self.should_end_session}
|
||||
response: dict[str, Any] = {"shouldEndSession": self.should_end_session}
|
||||
|
||||
if self.card is not None:
|
||||
response["card"] = self.card
|
||||
|
@ -1,20 +1,26 @@
|
||||
"""Describe logbook events."""
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.logbook import (
|
||||
LOGBOOK_ENTRY_ENTITY_ID,
|
||||
LOGBOOK_ENTRY_MESSAGE,
|
||||
LOGBOOK_ENTRY_NAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
|
||||
from .const import DOMAIN, EVENT_ALEXA_SMART_HOME
|
||||
|
||||
|
||||
@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."""
|
||||
|
||||
@callback
|
||||
def async_describe_logbook_event(event):
|
||||
def async_describe_logbook_event(event: Event) -> dict[str, Any]:
|
||||
"""Describe a logbook event."""
|
||||
data = event.data
|
||||
|
||||
|
@ -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
|
@ -1,6 +1,9 @@
|
||||
"""Alexa Resources and Assets."""
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AlexaGlobalCatalog:
|
||||
"""The Global Alexa catalog.
|
||||
|
||||
@ -207,36 +210,40 @@ class AlexaCapabilityResource:
|
||||
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."""
|
||||
self._resource_labels = []
|
||||
for label in labels:
|
||||
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 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 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.
|
||||
|
||||
Returns resource label objects for friendlyNames serialized.
|
||||
"""
|
||||
labels = []
|
||||
labels: list[dict[str, Any]] = []
|
||||
label_dict: dict[str, Any]
|
||||
for label in resources:
|
||||
if label in AlexaGlobalCatalog.__dict__.values():
|
||||
label = {"@type": "asset", "value": {"assetId": label}}
|
||||
label_dict = {"@type": "asset", "value": {"assetId": label}}
|
||||
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}
|
||||
|
||||
@ -247,22 +254,22 @@ class AlexaModeResource(AlexaCapabilityResource):
|
||||
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."""
|
||||
super().__init__(labels)
|
||||
self._supported_modes = []
|
||||
self._mode_ordered = ordered
|
||||
self._supported_modes: list[dict[str, Any]] = []
|
||||
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."""
|
||||
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.
|
||||
|
||||
Returns configuration for ModeResources friendlyNames serialized.
|
||||
"""
|
||||
mode_resources = []
|
||||
mode_resources: list[dict[str, Any]] = []
|
||||
for mode in self._supported_modes:
|
||||
result = {
|
||||
"value": mode["value"],
|
||||
@ -282,10 +289,17 @@ class AlexaPresetResource(AlexaCapabilityResource):
|
||||
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."""
|
||||
super().__init__(labels)
|
||||
self._presets = []
|
||||
self._presets: list[dict[str, Any]] = []
|
||||
self._minimum_value = min_value
|
||||
self._maximum_value = max_value
|
||||
self._precision = precision
|
||||
@ -293,16 +307,16 @@ class AlexaPresetResource(AlexaCapabilityResource):
|
||||
if unit in AlexaGlobalCatalog.__dict__.values():
|
||||
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."""
|
||||
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.
|
||||
|
||||
Returns configuration for PresetResources friendlyNames serialized.
|
||||
"""
|
||||
configuration = {
|
||||
configuration: dict[str, Any] = {
|
||||
"supportedRange": {
|
||||
"minimumValue": self._minimum_value,
|
||||
"maximumValue": self._maximum_value,
|
||||
@ -372,26 +386,28 @@ class AlexaSemantics:
|
||||
DIRECTIVE_MODE_SET_MODE = "SetMode"
|
||||
DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode"
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize an Alexa modeResource."""
|
||||
self._action_mappings = []
|
||||
self._state_mappings = []
|
||||
self._action_mappings: list[dict[str, Any]] = []
|
||||
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."""
|
||||
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."""
|
||||
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."""
|
||||
self._add_state_mapping(
|
||||
{"@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."""
|
||||
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."""
|
||||
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."""
|
||||
semantics = {}
|
||||
semantics: dict[str, Any] = {}
|
||||
if self._action_mappings:
|
||||
semantics[self.MAPPINGS_ACTION] = self._action_mappings
|
||||
if self._state_mappings:
|
||||
|
@ -1,17 +1,170 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
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 .handlers import HANDLERS
|
||||
from .messages import AlexaDirective
|
||||
from .state_report import AlexaDirective
|
||||
|
||||
_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.
|
||||
|
||||
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"
|
||||
|
||||
if context is None:
|
||||
context = ha.Context()
|
||||
context = Context()
|
||||
|
||||
directive = AlexaDirective(request)
|
||||
|
||||
@ -48,7 +201,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
|
||||
response = directive.error()
|
||||
except AlexaError as err:
|
||||
response = directive.error(
|
||||
error_type=err.error_type,
|
||||
error_type=str(err.error_type),
|
||||
error_message=err.error_message,
|
||||
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")
|
||||
|
||||
request_info = {"namespace": directive.namespace, "name": directive.name}
|
||||
request_info: dict[str, Any] = {
|
||||
"namespace": directive.namespace,
|
||||
"name": directive.name,
|
||||
}
|
||||
|
||||
if directive.has_endpoint:
|
||||
assert directive.entity_id is not None
|
||||
request_info["entity_id"] = directive.entity_id
|
||||
|
||||
hass.bus.async_fire(
|
||||
|
@ -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)
|
@ -2,27 +2,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
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 async_timeout
|
||||
|
||||
from homeassistant.components import event
|
||||
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.event import async_track_state_change
|
||||
from homeassistant.helpers.significant_change import create_checker
|
||||
import homeassistant.util.dt as dt_util
|
||||
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 .errors import NoTokenAvailable, RequireRelink
|
||||
from .messages import AlexaResponse
|
||||
from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .config import AbstractConfig
|
||||
@ -31,7 +44,202 @@ _LOGGER = logging.getLogger(__name__)
|
||||
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.
|
||||
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
old_state: str,
|
||||
old_attrs: dict,
|
||||
old_extra_arg: dict,
|
||||
old_attrs: dict[Any, Any] | MappingProxyType[Any, Any],
|
||||
old_extra_arg: Any,
|
||||
new_state: str,
|
||||
new_attrs: dict,
|
||||
new_extra_arg: dict,
|
||||
):
|
||||
new_attrs: dict[str, Any] | MappingProxyType[Any, Any],
|
||||
new_extra_arg: Any,
|
||||
) -> bool:
|
||||
"""Check if the serialized data has changed."""
|
||||
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,
|
||||
old_state: State | None,
|
||||
new_state: State | None,
|
||||
):
|
||||
) -> None:
|
||||
if not hass.is_running:
|
||||
return
|
||||
|
||||
@ -117,8 +325,13 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
headers: dict[str, Any] = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
endpoint = alexa_entity.alexa_id()
|
||||
|
||||
payload = {
|
||||
payload: dict[str, Any] = {
|
||||
API_CHANGE: {
|
||||
"cause": {"type": Cause.APP_INTERACTION},
|
||||
"properties": alexa_properties,
|
||||
@ -149,8 +362,9 @@ async def async_send_changereport_message(
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
try:
|
||||
async with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
async with timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(
|
||||
config.endpoint,
|
||||
headers=headers,
|
||||
@ -203,9 +417,9 @@ async def async_send_add_or_update_message(
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
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(
|
||||
name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload
|
||||
@ -226,6 +443,7 @@ async def async_send_add_or_update_message(
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
return await session.post(
|
||||
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()
|
||||
|
||||
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:
|
||||
domain = entity_id.split(".", 1)[0]
|
||||
@ -252,7 +470,10 @@ async def async_send_delete_message(
|
||||
|
||||
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(
|
||||
name="DeleteReport", namespace="Alexa.Discovery", payload=payload
|
||||
@ -261,19 +482,22 @@ async def async_send_delete_message(
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
return await session.post(
|
||||
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.
|
||||
|
||||
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-doorbelleventsource.html
|
||||
"""
|
||||
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()
|
||||
|
||||
@ -291,8 +515,9 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
||||
message_serialized = message.serialize()
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
assert config.endpoint is not None
|
||||
try:
|
||||
async with async_timeout.timeout(DEFAULT_TIMEOUT):
|
||||
async with timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.post(
|
||||
config.endpoint,
|
||||
headers=headers,
|
||||
|
@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["boto3", "botocore", "s3transfer"],
|
||||
"requirements": ["boto3==1.20.24"]
|
||||
"requirements": ["boto3==1.28.17"]
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
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.storage import Store
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
@ -100,7 +100,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
token_info = await oauth.get_access_token(code)
|
||||
except ambiclimate.AmbiclimateOauthError:
|
||||
_LOGGER.error("Failed to get access token", exc_info=True)
|
||||
_LOGGER.exception("Failed to get access token")
|
||||
return None
|
||||
|
||||
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
|
@ -5,7 +5,6 @@ from typing import Any
|
||||
|
||||
from aioambient import Websocket
|
||||
from aioambient.errors import WebsocketError
|
||||
from aioambient.util import get_public_device_id
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@ -19,11 +18,7 @@ from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .const import (
|
||||
@ -148,6 +143,7 @@ class AmbientStation:
|
||||
"""Define a handler to fire when the data is received."""
|
||||
mac = data["macAddress"]
|
||||
|
||||
# If data has not changed, don't update:
|
||||
if data == self.stations[mac][ATTR_LAST_DATA]:
|
||||
return
|
||||
|
||||
@ -196,71 +192,3 @@ class AmbientStation:
|
||||
async def ws_disconnect(self) -> None:
|
||||
"""Disconnect from the websocket."""
|
||||
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
|
||||
|
@ -14,8 +14,8 @@ from homeassistant.const import ATTR_NAME, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AmbientWeatherEntity
|
||||
from .const import ATTR_LAST_DATA, DOMAIN
|
||||
from .entity import AmbientWeatherEntity
|
||||
|
||||
TYPE_BATT1 = "batt1"
|
||||
TYPE_BATT10 = "batt10"
|
||||
@ -80,304 +80,303 @@ class AmbientBinarySensorDescription(
|
||||
BINARY_SENSOR_DESCRIPTIONS = (
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATTOUT,
|
||||
name="Battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT1,
|
||||
name="Battery 1",
|
||||
translation_key="battery_1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT2,
|
||||
name="Battery 2",
|
||||
translation_key="battery_2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT3,
|
||||
name="Battery 3",
|
||||
translation_key="battery_3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT4,
|
||||
name="Battery 4",
|
||||
translation_key="battery_4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT5,
|
||||
name="Battery 5",
|
||||
translation_key="battery_5",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT6,
|
||||
name="Battery 6",
|
||||
translation_key="battery_6",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT7,
|
||||
name="Battery 7",
|
||||
translation_key="battery_7",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT8,
|
||||
name="Battery 8",
|
||||
translation_key="battery_8",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT9,
|
||||
name="Battery 9",
|
||||
translation_key="battery_9",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATTIN,
|
||||
name="Interior battery",
|
||||
translation_key="interior_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT10,
|
||||
name="Battery 10",
|
||||
translation_key="battery_10",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK1,
|
||||
name="Leak detector battery 1",
|
||||
translation_key="leak_detector_battery_1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK2,
|
||||
name="Leak detector battery 2",
|
||||
translation_key="leak_detector_battery_2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK3,
|
||||
name="Leak detector battery 3",
|
||||
translation_key="leak_detector_battery_3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK4,
|
||||
name="Leak detector battery 4",
|
||||
translation_key="leak_detector_battery_4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM1,
|
||||
name="Soil monitor battery 1",
|
||||
translation_key="soil_monitor_battery_1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM2,
|
||||
name="Soil monitor battery 2",
|
||||
translation_key="soil_monitor_battery_2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM3,
|
||||
name="Soil monitor battery 3",
|
||||
translation_key="soil_monitor_battery_3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM4,
|
||||
name="Soil monitor battery 4",
|
||||
translation_key="soil_monitor_battery_4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM5,
|
||||
name="Soil monitor battery 5",
|
||||
translation_key="soil_monitor_battery_5",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM6,
|
||||
name="Soil monitor battery 6",
|
||||
translation_key="soil_monitor_battery_6",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM7,
|
||||
name="Soil monitor battery 7",
|
||||
translation_key="soil_monitor_battery_7",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM8,
|
||||
name="Soil monitor battery 8",
|
||||
translation_key="soil_monitor_battery_8",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM9,
|
||||
name="Soil monitor battery 9",
|
||||
translation_key="soil_monitor_battery_9",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM10,
|
||||
name="Soil monitor battery 10",
|
||||
translation_key="soil_monitor_battery_10",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_CO2,
|
||||
name="CO2 battery",
|
||||
translation_key="co2_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LIGHTNING,
|
||||
name="Lightning detector battery",
|
||||
translation_key="lightning_detector_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK1,
|
||||
name="Leak detector 1",
|
||||
translation_key="leak_detector_1",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK2,
|
||||
name="Leak detector 2",
|
||||
translation_key="leak_detector_2",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK3,
|
||||
name="Leak detector 3",
|
||||
translation_key="leak_detector_3",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK4,
|
||||
name="Leak detector 4",
|
||||
translation_key="leak_detector_4",
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_PM25IN_BATT,
|
||||
name="PM25 indoor battery",
|
||||
translation_key="pm25_indoor_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_PM25_BATT,
|
||||
name="PM25 battery",
|
||||
translation_key="pm25_battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY1,
|
||||
name="Relay 1",
|
||||
translation_key="relay_1",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY2,
|
||||
name="Relay 2",
|
||||
translation_key="relay_2",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY3,
|
||||
name="Relay 3",
|
||||
translation_key="relay_3",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY4,
|
||||
name="Relay 4",
|
||||
translation_key="relay_4",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY5,
|
||||
name="Relay 5",
|
||||
translation_key="relay_5",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY6,
|
||||
name="Relay 6",
|
||||
translation_key="relay_6",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY7,
|
||||
name="Relay 7",
|
||||
translation_key="relay_7",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY8,
|
||||
name="Relay 8",
|
||||
translation_key="relay_8",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY9,
|
||||
name="Relay 9",
|
||||
translation_key="relay_9",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY10,
|
||||
name="Relay 10",
|
||||
translation_key="relay_10",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=1,
|
||||
@ -409,9 +408,6 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity):
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Fetch new state data for the entity."""
|
||||
self._attr_is_on = (
|
||||
self._ambient.stations[self._mac_address][ATTR_LAST_DATA][
|
||||
self.entity_description.key
|
||||
]
|
||||
== self.entity_description.on_state
|
||||
)
|
||||
description = self.entity_description
|
||||
last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA]
|
||||
self._attr_is_on = last_data[description.key] == description.on_state
|
||||
|
70
homeassistant/components/ambient_station/entity.py
Normal file
70
homeassistant/components/ambient_station/entity.py
Normal 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
|
@ -28,8 +28,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
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 .entity import AmbientWeatherEntity
|
||||
|
||||
TYPE_24HOURRAININ = "24hourrainin"
|
||||
TYPE_AQI_PM25 = "aqi_pm25"
|
||||
@ -113,544 +114,536 @@ TYPE_YEARLYRAININ = "yearlyrainin"
|
||||
SENSOR_DESCRIPTIONS = (
|
||||
SensorEntityDescription(
|
||||
key=TYPE_24HOURRAININ,
|
||||
name="24 hr rain",
|
||||
translation_key="24_hour_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25,
|
||||
name="AQI PM2.5",
|
||||
translation_key="pm25_aqi",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_24H,
|
||||
name="AQI PM2.5 24h avg",
|
||||
translation_key="pm25_aqi_24h_average",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_IN,
|
||||
name="AQI PM2.5 indoor",
|
||||
translation_key="pm25_indoor_aqi",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_IN_24H,
|
||||
name="AQI PM2.5 indoor 24h avg",
|
||||
translation_key="pm25_indoor_aqi_24h_average",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMABSIN,
|
||||
name="Abs pressure",
|
||||
translation_key="absolute_pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMRELIN,
|
||||
name="Rel pressure",
|
||||
translation_key="relative_pressure",
|
||||
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_CO2,
|
||||
name="CO2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_DAILYRAININ,
|
||||
name="Daily rain",
|
||||
translation_key="daily_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_DEWPOINT,
|
||||
name="Dew point",
|
||||
translation_key="dew_point",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_EVENTRAININ,
|
||||
name="Event rain",
|
||||
translation_key="event_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_FEELSLIKE,
|
||||
name="Feels like",
|
||||
translation_key="feels_like",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HOURLYRAININ,
|
||||
name="Hourly rain rate",
|
||||
native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY10,
|
||||
name="Humidity 10",
|
||||
translation_key="humidity_10",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY1,
|
||||
name="Humidity 1",
|
||||
translation_key="humidity_1",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY2,
|
||||
name="Humidity 2",
|
||||
translation_key="humidity_2",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY3,
|
||||
name="Humidity 3",
|
||||
translation_key="humidity_3",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY4,
|
||||
name="Humidity 4",
|
||||
translation_key="humidity_4",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY5,
|
||||
name="Humidity 5",
|
||||
translation_key="humidity_5",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY6,
|
||||
name="Humidity 6",
|
||||
translation_key="humidity_6",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY7,
|
||||
name="Humidity 7",
|
||||
translation_key="humidity_7",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY8,
|
||||
name="Humidity 8",
|
||||
translation_key="humidity_8",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY9,
|
||||
name="Humidity 9",
|
||||
translation_key="humidity_9",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY,
|
||||
name="Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITYIN,
|
||||
name="Humidity in",
|
||||
translation_key="humidity_indoor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LASTRAIN,
|
||||
name="Last rain",
|
||||
translation_key="last_rain",
|
||||
icon="mdi:water",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LIGHTNING_PER_DAY,
|
||||
name="Lightning strikes per day",
|
||||
translation_key="lightning_strikes_per_day",
|
||||
icon="mdi:lightning-bolt",
|
||||
native_unit_of_measurement="strikes",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LIGHTNING_PER_HOUR,
|
||||
name="Lightning strikes per hour",
|
||||
translation_key="lightning_strikes_per_hour",
|
||||
icon="mdi:lightning-bolt",
|
||||
native_unit_of_measurement="strikes",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MAXDAILYGUST,
|
||||
name="Max gust",
|
||||
translation_key="max_gust",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MONTHLYRAININ,
|
||||
name="Monthly rain",
|
||||
translation_key="monthly_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_24H,
|
||||
name="PM25 24h avg",
|
||||
translation_key="pm25_24h_average",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_IN,
|
||||
name="PM25 indoor",
|
||||
translation_key="pm25_indoor",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
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,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25,
|
||||
name="PM25",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM10,
|
||||
name="Soil humidity 10",
|
||||
translation_key="soil_humidity_10",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM1,
|
||||
name="Soil humidity 1",
|
||||
translation_key="soil_humidity_1",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM2,
|
||||
name="Soil humidity 2",
|
||||
translation_key="soil_humidity_2",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM3,
|
||||
name="Soil humidity 3",
|
||||
translation_key="soil_humidity_3",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM4,
|
||||
name="Soil humidity 4",
|
||||
translation_key="soil_humidity_4",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM5,
|
||||
name="Soil humidity 5",
|
||||
translation_key="soil_humidity_5",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM6,
|
||||
name="Soil humidity 6",
|
||||
translation_key="soil_humidity_6",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM7,
|
||||
name="Soil humidity 7",
|
||||
translation_key="soil_humidity_7",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM8,
|
||||
name="Soil humidity 8",
|
||||
translation_key="soil_humidity_8",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM9,
|
||||
name="Soil humidity 9",
|
||||
translation_key="soil_humidity_9",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP10F,
|
||||
name="Soil temp 10",
|
||||
translation_key="soil_temperature_10",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP1F,
|
||||
name="Soil temp 1",
|
||||
translation_key="soil_temperature_1",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP2F,
|
||||
name="Soil temp 2",
|
||||
translation_key="soil_temperature_2",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP3F,
|
||||
name="Soil temp 3",
|
||||
translation_key="soil_temperature_3",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP4F,
|
||||
name="Soil temp 4",
|
||||
translation_key="soil_temperature_4",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP5F,
|
||||
name="Soil temp 5",
|
||||
translation_key="soil_temperature_5",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP6F,
|
||||
name="Soil temp 6",
|
||||
translation_key="soil_temperature_6",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP7F,
|
||||
name="Soil temp 7",
|
||||
translation_key="soil_temperature_7",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP8F,
|
||||
name="Soil temp 8",
|
||||
translation_key="soil_temperature_8",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP9F,
|
||||
name="Soil temp 9",
|
||||
translation_key="soil_temperature_9",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOLARRADIATION,
|
||||
name="Solar rad",
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
device_class=SensorDeviceClass.IRRADIANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOLARRADIATION_LX,
|
||||
name="Solar rad",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP10F,
|
||||
name="Temp 10",
|
||||
translation_key="temperature_10",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP1F,
|
||||
name="Temp 1",
|
||||
translation_key="temperature_1",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP2F,
|
||||
name="Temp 2",
|
||||
translation_key="temperature_2",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP3F,
|
||||
name="Temp 3",
|
||||
translation_key="temperature_3",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP4F,
|
||||
name="Temp 4",
|
||||
translation_key="temperature_4",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP5F,
|
||||
name="Temp 5",
|
||||
translation_key="temperature_5",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP6F,
|
||||
name="Temp 6",
|
||||
translation_key="temperature_6",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP7F,
|
||||
name="Temp 7",
|
||||
translation_key="temperature_7",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP8F,
|
||||
name="Temp 8",
|
||||
translation_key="temperature_8",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP9F,
|
||||
name="Temp 9",
|
||||
translation_key="temperature_9",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMPF,
|
||||
name="Temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMPINF,
|
||||
name="Inside temp",
|
||||
translation_key="inside_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TOTALRAININ,
|
||||
name="Lifetime rain",
|
||||
translation_key="lifetime_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_UV,
|
||||
name="UV index",
|
||||
translation_key="uv_index",
|
||||
native_unit_of_measurement="Index",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WEEKLYRAININ,
|
||||
name="Weekly rain",
|
||||
translation_key="weekly_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR,
|
||||
name="Wind dir",
|
||||
translation_key="wind_direction",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG10M,
|
||||
name="Wind dir avg 10m",
|
||||
translation_key="wind_direction_average_10m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG2M,
|
||||
name="Wind dir avg 2m",
|
||||
translation_key="wind_direction_average_2m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTDIR,
|
||||
name="Gust dir",
|
||||
translation_key="wind_gust_direction",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTMPH,
|
||||
name="Wind gust",
|
||||
translation_key="wind_gust",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPDMPH_AVG10M,
|
||||
name="Wind avg 10m",
|
||||
translation_key="wind_average_10m",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPDMPH_AVG2M,
|
||||
name="Wind avg 2m",
|
||||
translation_key="wind_average_2m",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPEEDMPH,
|
||||
name="Wind speed",
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_YEARLYRAININ,
|
||||
name="Yearly rain",
|
||||
translation_key="yearly_rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@ -694,11 +687,9 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Fetch new state data for the sensor."""
|
||||
raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][
|
||||
self.entity_description.key
|
||||
]
|
||||
|
||||
if self.entity_description.key == TYPE_LASTRAIN:
|
||||
key = self.entity_description.key
|
||||
raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][key]
|
||||
if key == TYPE_LASTRAIN:
|
||||
self._attr_native_value = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
else:
|
||||
self._attr_native_value = raw
|
||||
|
@ -16,5 +16,356 @@
|
||||
"abort": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from dataclasses import asdict as dataclass_asdict, dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components import hassio
|
||||
from homeassistant.components.api import ATTR_INSTALLATION_TYPE
|
||||
@ -22,9 +22,7 @@ from homeassistant.components.recorder import (
|
||||
get_instance as get_recorder_instance,
|
||||
)
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@ -315,7 +313,7 @@ class Analytics:
|
||||
)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
async with timeout(30):
|
||||
response = await self.session.post(self.endpoint, json=payload)
|
||||
if response.status == 200:
|
||||
LOGGER.info(
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.const import (
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
)
|
||||
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 .const import DOMAIN
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Base class for Android IP Webcam entities."""
|
||||
|
||||
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 .const import DOMAIN
|
||||
|
@ -32,9 +32,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
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.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
import logging
|
||||
|
||||
from androidtvremote2 import (
|
||||
@ -10,7 +11,6 @@ from androidtvremote2 import (
|
||||
ConnectionClosed,
|
||||
InvalidAuth,
|
||||
)
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(5.0):
|
||||
async with timeout(5.0):
|
||||
await api.async_connect()
|
||||
except InvalidAuth as exc:
|
||||
# The Android TV is hard reset or the certificate and key files were deleted.
|
||||
|
@ -12,8 +12,12 @@ from androidtvremote2 import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
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.core import callback
|
||||
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."""
|
||||
|
||||
VERSION = 1
|
||||
@ -43,7 +47,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a new AndroidTVRemoteConfigFlow."""
|
||||
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.name: str | None = None
|
||||
self.mac: str | None = None
|
||||
@ -192,19 +196,15 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> config_entries.OptionsFlow:
|
||||
config_entry: ConfigEntry,
|
||||
) -> AndroidTVRemoteOptionsFlowHandler:
|
||||
"""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."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
|
@ -7,8 +7,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
@ -6,7 +6,7 @@ import logging
|
||||
from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
|
||||
|
||||
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.helpers import aiohttp_client
|
||||
|
||||
@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
device[1],
|
||||
api.jwt,
|
||||
)
|
||||
for device in entry.data["devices"]
|
||||
for device in entry.data[CONF_DEVICES]
|
||||
]
|
||||
try:
|
||||
new_devices = await api.get_devices()
|
||||
@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
**{"devices": serialize_device_list(devices)},
|
||||
**{CONF_DEVICES: serialize_device_list(devices)},
|
||||
},
|
||||
)
|
||||
coordinators = [AnovaCoordinator(hass, device) for device in devices]
|
||||
|
@ -4,16 +4,16 @@ from __future__ import annotations
|
||||
from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME
|
||||
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 .util import serialize_device_list
|
||||
|
||||
|
||||
class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Sets up a config flow for Anova."""
|
||||
|
||||
VERSION = 1
|
||||
@ -25,7 +25,7 @@ class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
api = AnovaApi(
|
||||
aiohttp_client.async_get_clientsession(self.hass),
|
||||
async_get_clientsession(self.hass),
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
@ -48,7 +48,7 @@ class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
CONF_USERNAME: api.username,
|
||||
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
Loading…
x
Reference in New Issue
Block a user