forked from home-assistant/core
Compare commits
3 Commits
2024.1.0b1
...
fix-host-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83458d24c7 | ||
|
|
df6d43adc4 | ||
|
|
5cbfc1c224 |
45
.coveragerc
45
.coveragerc
@@ -173,7 +173,6 @@ omit =
|
||||
homeassistant/components/coinbase/sensor.py
|
||||
homeassistant/components/comed_hourly_pricing/sensor.py
|
||||
homeassistant/components/comelit/__init__.py
|
||||
homeassistant/components/comelit/alarm_control_panel.py
|
||||
homeassistant/components/comelit/const.py
|
||||
homeassistant/components/comelit/cover.py
|
||||
homeassistant/components/comelit/coordinator.py
|
||||
@@ -338,6 +337,7 @@ omit =
|
||||
homeassistant/components/escea/__init__.py
|
||||
homeassistant/components/escea/climate.py
|
||||
homeassistant/components/escea/discovery.py
|
||||
homeassistant/components/esphome/bluetooth/*
|
||||
homeassistant/components/esphome/manager.py
|
||||
homeassistant/components/etherscan/sensor.py
|
||||
homeassistant/components/eufy/*
|
||||
@@ -364,6 +364,8 @@ omit =
|
||||
homeassistant/components/faa_delays/binary_sensor.py
|
||||
homeassistant/components/faa_delays/coordinator.py
|
||||
homeassistant/components/familyhub/camera.py
|
||||
homeassistant/components/fastdotcom/sensor.py
|
||||
homeassistant/components/fastdotcom/__init__.py
|
||||
homeassistant/components/ffmpeg/camera.py
|
||||
homeassistant/components/fibaro/__init__.py
|
||||
homeassistant/components/fibaro/binary_sensor.py
|
||||
@@ -402,9 +404,6 @@ omit =
|
||||
homeassistant/components/fjaraskupan/sensor.py
|
||||
homeassistant/components/fleetgo/device_tracker.py
|
||||
homeassistant/components/flexit/climate.py
|
||||
homeassistant/components/flexit_bacnet/__init__.py
|
||||
homeassistant/components/flexit_bacnet/const.py
|
||||
homeassistant/components/flexit_bacnet/climate.py
|
||||
homeassistant/components/flic/binary_sensor.py
|
||||
homeassistant/components/flick_electric/__init__.py
|
||||
homeassistant/components/flick_electric/sensor.py
|
||||
@@ -420,7 +419,6 @@ omit =
|
||||
homeassistant/components/fortios/device_tracker.py
|
||||
homeassistant/components/foscam/__init__.py
|
||||
homeassistant/components/foscam/camera.py
|
||||
homeassistant/components/foscam/coordinator.py
|
||||
homeassistant/components/foursquare/*
|
||||
homeassistant/components/free_mobile/notify.py
|
||||
homeassistant/components/freebox/camera.py
|
||||
@@ -538,7 +536,6 @@ omit =
|
||||
homeassistant/components/hvv_departures/binary_sensor.py
|
||||
homeassistant/components/hvv_departures/sensor.py
|
||||
homeassistant/components/ialarm/alarm_control_panel.py
|
||||
homeassistant/components/iammeter/const.py
|
||||
homeassistant/components/iammeter/sensor.py
|
||||
homeassistant/components/iaqualink/binary_sensor.py
|
||||
homeassistant/components/iaqualink/climate.py
|
||||
@@ -636,6 +633,8 @@ omit =
|
||||
homeassistant/components/kodi/browse_media.py
|
||||
homeassistant/components/kodi/media_player.py
|
||||
homeassistant/components/kodi/notify.py
|
||||
homeassistant/components/komfovent/__init__.py
|
||||
homeassistant/components/komfovent/climate.py
|
||||
homeassistant/components/konnected/__init__.py
|
||||
homeassistant/components/konnected/panel.py
|
||||
homeassistant/components/konnected/switch.py
|
||||
@@ -757,9 +756,6 @@ omit =
|
||||
homeassistant/components/motion_blinds/cover.py
|
||||
homeassistant/components/motion_blinds/entity.py
|
||||
homeassistant/components/motion_blinds/sensor.py
|
||||
homeassistant/components/motionmount/__init__.py
|
||||
homeassistant/components/motionmount/entity.py
|
||||
homeassistant/components/motionmount/number.py
|
||||
homeassistant/components/mpd/media_player.py
|
||||
homeassistant/components/mqtt_room/sensor.py
|
||||
homeassistant/components/msteams/notify.py
|
||||
@@ -805,8 +801,7 @@ omit =
|
||||
homeassistant/components/netgear/sensor.py
|
||||
homeassistant/components/netgear/switch.py
|
||||
homeassistant/components/netgear/update.py
|
||||
homeassistant/components/netgear_lte/__init__.py
|
||||
homeassistant/components/netgear_lte/notify.py
|
||||
homeassistant/components/netgear_lte/*
|
||||
homeassistant/components/netio/switch.py
|
||||
homeassistant/components/neurio_energy/sensor.py
|
||||
homeassistant/components/nexia/climate.py
|
||||
@@ -907,9 +902,6 @@ omit =
|
||||
homeassistant/components/opple/light.py
|
||||
homeassistant/components/oru/*
|
||||
homeassistant/components/orvibo/switch.py
|
||||
homeassistant/components/osoenergy/__init__.py
|
||||
homeassistant/components/osoenergy/const.py
|
||||
homeassistant/components/osoenergy/water_heater.py
|
||||
homeassistant/components/osramlightify/light.py
|
||||
homeassistant/components/otp/sensor.py
|
||||
homeassistant/components/overkiz/__init__.py
|
||||
@@ -1036,12 +1028,6 @@ omit =
|
||||
homeassistant/components/recorder/repack.py
|
||||
homeassistant/components/recswitch/switch.py
|
||||
homeassistant/components/reddit/sensor.py
|
||||
homeassistant/components/refoss/__init__.py
|
||||
homeassistant/components/refoss/bridge.py
|
||||
homeassistant/components/refoss/coordinator.py
|
||||
homeassistant/components/refoss/entity.py
|
||||
homeassistant/components/refoss/switch.py
|
||||
homeassistant/components/refoss/util.py
|
||||
homeassistant/components/rejseplanen/sensor.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote_rpi_gpio/*
|
||||
@@ -1225,7 +1211,6 @@ omit =
|
||||
homeassistant/components/starline/__init__.py
|
||||
homeassistant/components/starline/account.py
|
||||
homeassistant/components/starline/binary_sensor.py
|
||||
homeassistant/components/starline/button.py
|
||||
homeassistant/components/starline/device_tracker.py
|
||||
homeassistant/components/starline/entity.py
|
||||
homeassistant/components/starline/lock.py
|
||||
@@ -1243,12 +1228,8 @@ omit =
|
||||
homeassistant/components/stream/fmp4utils.py
|
||||
homeassistant/components/stream/hls.py
|
||||
homeassistant/components/stream/worker.py
|
||||
homeassistant/components/streamlabswater/__init__.py
|
||||
homeassistant/components/streamlabswater/binary_sensor.py
|
||||
homeassistant/components/streamlabswater/coordinator.py
|
||||
homeassistant/components/streamlabswater/sensor.py
|
||||
homeassistant/components/suez_water/__init__.py
|
||||
homeassistant/components/suez_water/sensor.py
|
||||
homeassistant/components/streamlabswater/*
|
||||
homeassistant/components/suez_water/*
|
||||
homeassistant/components/supervisord/sensor.py
|
||||
homeassistant/components/supla/*
|
||||
homeassistant/components/surepetcare/__init__.py
|
||||
@@ -1256,8 +1237,6 @@ omit =
|
||||
homeassistant/components/surepetcare/entity.py
|
||||
homeassistant/components/surepetcare/sensor.py
|
||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||
homeassistant/components/swiss_public_transport/__init__.py
|
||||
homeassistant/components/swiss_public_transport/coordinator.py
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.py
|
||||
homeassistant/components/switchbee/__init__.py
|
||||
@@ -1309,9 +1288,7 @@ omit =
|
||||
homeassistant/components/system_bridge/notify.py
|
||||
homeassistant/components/system_bridge/sensor.py
|
||||
homeassistant/components/system_bridge/update.py
|
||||
homeassistant/components/systemmonitor/__init__.py
|
||||
homeassistant/components/systemmonitor/sensor.py
|
||||
homeassistant/components/systemmonitor/util.py
|
||||
homeassistant/components/tado/__init__.py
|
||||
homeassistant/components/tado/binary_sensor.py
|
||||
homeassistant/components/tado/climate.py
|
||||
@@ -1400,6 +1377,10 @@ omit =
|
||||
homeassistant/components/tradfri/light.py
|
||||
homeassistant/components/tradfri/sensor.py
|
||||
homeassistant/components/tradfri/switch.py
|
||||
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
|
||||
@@ -1434,8 +1415,6 @@ omit =
|
||||
homeassistant/components/ukraine_alarm/__init__.py
|
||||
homeassistant/components/ukraine_alarm/binary_sensor.py
|
||||
homeassistant/components/unifiled/*
|
||||
homeassistant/components/unifi_direct/__init__.py
|
||||
homeassistant/components/unifi_direct/device_tracker.py
|
||||
homeassistant/components/upb/__init__.py
|
||||
homeassistant/components/upb/light.py
|
||||
homeassistant/components/upc_connect/*
|
||||
|
||||
13
.github/workflows/builder.yml
vendored
13
.github/workflows/builder.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.12.0
|
||||
uses: home-assistant/builder@2023.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -247,7 +247,6 @@ jobs:
|
||||
- raspberrypi3-64
|
||||
- raspberrypi4
|
||||
- raspberrypi4-64
|
||||
- raspberrypi5-64
|
||||
- tinker
|
||||
- yellow
|
||||
- green
|
||||
@@ -274,7 +273,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.12.0
|
||||
uses: home-assistant/builder@2023.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -331,7 +330,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.3.0
|
||||
uses: sigstore/cosign-installer@v3.2.0
|
||||
with:
|
||||
cosign-release: "v2.0.2"
|
||||
|
||||
|
||||
26
.github/workflows/ci.yaml
vendored
26
.github/workflows/ci.yaml
vendored
@@ -36,7 +36,7 @@ env:
|
||||
CACHE_VERSION: 5
|
||||
PIP_CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 6
|
||||
HA_SHORT_VERSION: "2024.1"
|
||||
HA_SHORT_VERSION: "2023.12"
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
ALL_PYTHON_VERSIONS: "['3.11', '3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -443,7 +443,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -511,7 +511,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -543,7 +543,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -576,7 +576,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -620,7 +620,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -702,7 +702,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -854,7 +854,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -978,7 +978,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -29,11 +29,11 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.22.12
|
||||
uses: github/codeql-action/init@v2.22.8
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.22.12
|
||||
uses: github/codeql-action/analyze@v2.22.8
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
18
.github/workflows/stale.yml
vendored
18
.github/workflows/stale.yml
vendored
@@ -11,16 +11,16 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# The 60 day stale policy for PRs
|
||||
# The 90 day stale policy for PRs
|
||||
# Used for:
|
||||
# - PRs
|
||||
# - No PRs marked as no-stale
|
||||
# - No issues (-1)
|
||||
- name: 60 days stale PRs policy
|
||||
uses: actions/stale@v9.0.0
|
||||
- name: 90 days stale PRs policy
|
||||
uses: actions/stale@v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 60
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
@@ -33,11 +33,7 @@ jobs:
|
||||
pull request has been automatically marked as stale because of that
|
||||
and will be closed if no further activity occurs within 7 days.
|
||||
|
||||
If you are the author of this PR, please leave a comment if you want
|
||||
to keep it open. Also, please rebase your PR onto the latest dev
|
||||
branch to ensure that it's up to date with the latest changes.
|
||||
|
||||
Thank you for your contribution!
|
||||
Thank you for your contributions.
|
||||
|
||||
# Generate a token for the GitHub App, we use this method to avoid
|
||||
# hitting API limits for our GitHub actions + have a higher rate limit.
|
||||
@@ -57,7 +53,7 @@ jobs:
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: 90 days stale issues
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v8.0.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
days-before-stale: 90
|
||||
@@ -87,7 +83,7 @@ jobs:
|
||||
# - No Issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: Needs more information stale issues policy
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v8.0.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
only-labels: "needs-more-information"
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
uses: actions/setup-python@v4.7.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.8
|
||||
rev: v0.1.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -43,14 +43,11 @@ homeassistant.components.abode.*
|
||||
homeassistant.components.accuweather.*
|
||||
homeassistant.components.acer_projector.*
|
||||
homeassistant.components.actiontec.*
|
||||
homeassistant.components.adax.*
|
||||
homeassistant.components.adguard.*
|
||||
homeassistant.components.aftership.*
|
||||
homeassistant.components.air_quality.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airvisual.*
|
||||
homeassistant.components.airvisual_pro.*
|
||||
homeassistant.components.airzone.*
|
||||
homeassistant.components.airzone_cloud.*
|
||||
homeassistant.components.aladdin_connect.*
|
||||
@@ -62,14 +59,10 @@ homeassistant.components.ambient_station.*
|
||||
homeassistant.components.amcrest.*
|
||||
homeassistant.components.ampio.*
|
||||
homeassistant.components.analytics.*
|
||||
homeassistant.components.android_ip_webcam.*
|
||||
homeassistant.components.androidtv_remote.*
|
||||
homeassistant.components.anova.*
|
||||
homeassistant.components.anthemav.*
|
||||
homeassistant.components.apcupsd.*
|
||||
homeassistant.components.apprise.*
|
||||
homeassistant.components.aqualogic.*
|
||||
homeassistant.components.aranet.*
|
||||
homeassistant.components.aseko_pool_live.*
|
||||
homeassistant.components.assist_pipeline.*
|
||||
homeassistant.components.asuswrt.*
|
||||
@@ -82,7 +75,6 @@ homeassistant.components.bayesian.*
|
||||
homeassistant.components.binary_sensor.*
|
||||
homeassistant.components.bitcoin.*
|
||||
homeassistant.components.blockchain.*
|
||||
homeassistant.components.blue_current.*
|
||||
homeassistant.components.bluetooth.*
|
||||
homeassistant.components.bluetooth_tracker.*
|
||||
homeassistant.components.bmw_connected_drive.*
|
||||
@@ -125,12 +117,9 @@ homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
homeassistant.components.emulated_hue.*
|
||||
homeassistant.components.energy.*
|
||||
homeassistant.components.enigma2.*
|
||||
homeassistant.components.esphome.*
|
||||
homeassistant.components.event.*
|
||||
homeassistant.components.evil_genius_labs.*
|
||||
homeassistant.components.evohome.*
|
||||
homeassistant.components.faa_delays.*
|
||||
homeassistant.components.fan.*
|
||||
homeassistant.components.fastdotcom.*
|
||||
homeassistant.components.feedreader.*
|
||||
@@ -138,7 +127,6 @@ homeassistant.components.file_upload.*
|
||||
homeassistant.components.filesize.*
|
||||
homeassistant.components.filter.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.flexit_bacnet.*
|
||||
homeassistant.components.flux_led.*
|
||||
homeassistant.components.forecast_solar.*
|
||||
homeassistant.components.fritz.*
|
||||
@@ -162,7 +150,6 @@ homeassistant.components.hardkernel.*
|
||||
homeassistant.components.hardware.*
|
||||
homeassistant.components.here_travel_time.*
|
||||
homeassistant.components.history.*
|
||||
homeassistant.components.holiday.*
|
||||
homeassistant.components.homeassistant.exposed_entities
|
||||
homeassistant.components.homeassistant.triggers.event
|
||||
homeassistant.components.homeassistant_alerts.*
|
||||
@@ -241,7 +228,6 @@ homeassistant.components.modbus.*
|
||||
homeassistant.components.modem_callerid.*
|
||||
homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
homeassistant.components.motionmount.*
|
||||
homeassistant.components.mqtt.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.nam.*
|
||||
@@ -278,7 +264,6 @@ homeassistant.components.proximity.*
|
||||
homeassistant.components.prusalink.*
|
||||
homeassistant.components.pure_energie.*
|
||||
homeassistant.components.purpleair.*
|
||||
homeassistant.components.pushbullet.*
|
||||
homeassistant.components.pvoutput.*
|
||||
homeassistant.components.qnap_qsw.*
|
||||
homeassistant.components.radarr.*
|
||||
@@ -328,8 +313,6 @@ homeassistant.components.statistics.*
|
||||
homeassistant.components.steamist.*
|
||||
homeassistant.components.stookalert.*
|
||||
homeassistant.components.stream.*
|
||||
homeassistant.components.streamlabswater.*
|
||||
homeassistant.components.suez_water.*
|
||||
homeassistant.components.sun.*
|
||||
homeassistant.components.surepetcare.*
|
||||
homeassistant.components.switch.*
|
||||
@@ -340,7 +323,6 @@ homeassistant.components.synology_dsm.*
|
||||
homeassistant.components.systemmonitor.*
|
||||
homeassistant.components.tag.*
|
||||
homeassistant.components.tailscale.*
|
||||
homeassistant.components.tailwind.*
|
||||
homeassistant.components.tami4.*
|
||||
homeassistant.components.tautulli.*
|
||||
homeassistant.components.tcp.*
|
||||
@@ -371,7 +353,6 @@ homeassistant.components.uptimerobot.*
|
||||
homeassistant.components.usb.*
|
||||
homeassistant.components.vacuum.*
|
||||
homeassistant.components.vallox.*
|
||||
homeassistant.components.valve.*
|
||||
homeassistant.components.velbus.*
|
||||
homeassistant.components.vlc_telnet.*
|
||||
homeassistant.components.wake_on_lan.*
|
||||
|
||||
66
CODEOWNERS
66
CODEOWNERS
@@ -86,8 +86,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/anova/ @Lash-L
|
||||
/homeassistant/components/anthemav/ @hyralex
|
||||
/tests/components/anthemav/ @hyralex
|
||||
/homeassistant/components/aosmith/ @bdr99
|
||||
/tests/components/aosmith/ @bdr99
|
||||
/homeassistant/components/apache_kafka/ @bachya
|
||||
/tests/components/apache_kafka/ @bachya
|
||||
/homeassistant/components/apcupsd/ @yuxincs
|
||||
@@ -155,8 +153,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/blebox/ @bbx-a @riokuu
|
||||
/homeassistant/components/blink/ @fronzbot @mkmer
|
||||
/tests/components/blink/ @fronzbot @mkmer
|
||||
/homeassistant/components/blue_current/ @Floris272 @gleeuwen
|
||||
/tests/components/blue_current/ @Floris272 @gleeuwen
|
||||
/homeassistant/components/bluemaestro/ @bdraco
|
||||
/tests/components/bluemaestro/ @bdraco
|
||||
/homeassistant/components/blueprint/ @home-assistant/core
|
||||
@@ -197,8 +193,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/camera/ @home-assistant/core
|
||||
/homeassistant/components/cast/ @emontnemery
|
||||
/tests/components/cast/ @emontnemery
|
||||
/homeassistant/components/ccm15/ @ocalvo
|
||||
/tests/components/ccm15/ @ocalvo
|
||||
/homeassistant/components/cert_expiry/ @jjlawren
|
||||
/tests/components/cert_expiry/ @jjlawren
|
||||
/homeassistant/components/circuit/ @braam
|
||||
@@ -211,8 +205,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/cloud/ @home-assistant/cloud
|
||||
/homeassistant/components/cloudflare/ @ludeeus @ctalkington
|
||||
/tests/components/cloudflare/ @ludeeus @ctalkington
|
||||
/homeassistant/components/co2signal/ @jpbede @VIKTORVAV99
|
||||
/tests/components/co2signal/ @jpbede @VIKTORVAV99
|
||||
/homeassistant/components/co2signal/ @jpbede
|
||||
/tests/components/co2signal/ @jpbede
|
||||
/homeassistant/components/coinbase/ @tombrien
|
||||
/tests/components/coinbase/ @tombrien
|
||||
/homeassistant/components/color_extractor/ @GenericStudent
|
||||
@@ -265,8 +259,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||
/homeassistant/components/derivative/ @afaucogney
|
||||
/tests/components/derivative/ @afaucogney
|
||||
/homeassistant/components/devialet/ @fwestenberg
|
||||
/tests/components/devialet/ @fwestenberg
|
||||
/homeassistant/components/device_automation/ @home-assistant/core
|
||||
/tests/components/device_automation/ @home-assistant/core
|
||||
/homeassistant/components/device_tracker/ @home-assistant/core
|
||||
@@ -301,8 +293,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dormakaba_dkey/ @emontnemery
|
||||
/homeassistant/components/dremel_3d_printer/ @tkdrob
|
||||
/tests/components/dremel_3d_printer/ @tkdrob
|
||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
||||
/tests/components/dsmr/ @Robbie1221 @frenck
|
||||
/homeassistant/components/dsmr_reader/ @depl0y @glodenox
|
||||
@@ -352,7 +342,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/energy/ @home-assistant/core
|
||||
/homeassistant/components/energyzero/ @klaasnicolaas
|
||||
/tests/components/energyzero/ @klaasnicolaas
|
||||
/homeassistant/components/enigma2/ @autinerd
|
||||
/homeassistant/components/enigma2/ @fbradyirl
|
||||
/homeassistant/components/enocean/ @bdurrer
|
||||
/tests/components/enocean/ @bdurrer
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
||||
@@ -403,8 +393,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/fivem/ @Sander0542
|
||||
/homeassistant/components/fjaraskupan/ @elupus
|
||||
/tests/components/fjaraskupan/ @elupus
|
||||
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||
/tests/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||
/homeassistant/components/flick_electric/ @ZephireNZ
|
||||
/tests/components/flick_electric/ @ZephireNZ
|
||||
/homeassistant/components/flipr/ @cnico
|
||||
@@ -420,8 +408,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/forked_daapd/ @uvjustin
|
||||
/tests/components/forked_daapd/ @uvjustin
|
||||
/homeassistant/components/fortios/ @kimfrellsen
|
||||
/homeassistant/components/foscam/ @skgsergio @krmarien
|
||||
/tests/components/foscam/ @skgsergio @krmarien
|
||||
/homeassistant/components/foscam/ @skgsergio
|
||||
/tests/components/foscam/ @skgsergio
|
||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
@@ -530,8 +518,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/hive/ @Rendili @KJonline
|
||||
/homeassistant/components/hlk_sw16/ @jameshilliard
|
||||
/tests/components/hlk_sw16/ @jameshilliard
|
||||
/homeassistant/components/holiday/ @jrieger
|
||||
/tests/components/holiday/ @jrieger
|
||||
/homeassistant/components/home_connect/ @DavidMStraub
|
||||
/tests/components/home_connect/ @DavidMStraub
|
||||
/homeassistant/components/home_plus_control/ @chemaaa
|
||||
@@ -675,6 +661,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||
/homeassistant/components/kodi/ @OnFreund
|
||||
/tests/components/kodi/ @OnFreund
|
||||
/homeassistant/components/komfovent/ @ProstoSanja
|
||||
/tests/components/komfovent/ @ProstoSanja
|
||||
/homeassistant/components/konnected/ @heythisisnate
|
||||
/tests/components/konnected/ @heythisisnate
|
||||
/homeassistant/components/kostal_plenticore/ @stegm
|
||||
@@ -815,8 +803,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/motion_blinds/ @starkillerOG
|
||||
/homeassistant/components/motioneye/ @dermotduffy
|
||||
/tests/components/motioneye/ @dermotduffy
|
||||
/homeassistant/components/motionmount/ @RJPoelstra
|
||||
/tests/components/motionmount/ @RJPoelstra
|
||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh
|
||||
/tests/components/mqtt/ @emontnemery @jbouwh
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
@@ -847,7 +833,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
|
||||
/homeassistant/components/netgear_lte/ @tkdrob
|
||||
/tests/components/netgear_lte/ @tkdrob
|
||||
/homeassistant/components/network/ @home-assistant/core
|
||||
/tests/components/network/ @home-assistant/core
|
||||
/homeassistant/components/nexia/ @bdraco
|
||||
@@ -941,8 +926,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/oralb/ @bdraco @Lash-L
|
||||
/tests/components/oralb/ @bdraco @Lash-L
|
||||
/homeassistant/components/oru/ @bvlaicu
|
||||
/homeassistant/components/osoenergy/ @osohotwateriot
|
||||
/tests/components/osoenergy/ @osohotwateriot
|
||||
/homeassistant/components/otbr/ @home-assistant/core
|
||||
/tests/components/otbr/ @home-assistant/core
|
||||
/homeassistant/components/ourgroceries/ @OnFreund
|
||||
@@ -1002,8 +985,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/proximity/ @mib1185
|
||||
/tests/components/proximity/ @mib1185
|
||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
||||
/homeassistant/components/prusalink/ @balloob @Skaronator
|
||||
/tests/components/prusalink/ @balloob @Skaronator
|
||||
/homeassistant/components/prusalink/ @balloob
|
||||
/tests/components/prusalink/ @balloob
|
||||
/homeassistant/components/ps4/ @ktnrg45
|
||||
/tests/components/ps4/ @ktnrg45
|
||||
/homeassistant/components/pure_energie/ @klaasnicolaas
|
||||
@@ -1020,8 +1003,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/pvoutput/ @frenck
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse
|
||||
/tests/components/qbittorrent/ @geoffreylagaisse
|
||||
/homeassistant/components/qingping/ @bdraco @skgsergio
|
||||
/tests/components/qingping/ @bdraco @skgsergio
|
||||
/homeassistant/components/qld_bushfire/ @exxamalte
|
||||
@@ -1063,8 +1046,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/recorder/ @home-assistant/core
|
||||
/homeassistant/components/recovery_mode/ @home-assistant/core
|
||||
/tests/components/recovery_mode/ @home-assistant/core
|
||||
/homeassistant/components/refoss/ @ashionky
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/rejseplanen/ @DarkFox
|
||||
/homeassistant/components/remote/ @home-assistant/core
|
||||
/tests/components/remote/ @home-assistant/core
|
||||
@@ -1077,8 +1058,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/repairs/ @home-assistant/core
|
||||
/tests/components/repairs/ @home-assistant/core
|
||||
/homeassistant/components/repetier/ @ShadowBr0ther
|
||||
/homeassistant/components/rest_command/ @jpbede
|
||||
/tests/components/rest_command/ @jpbede
|
||||
/homeassistant/components/rflink/ @javicalle
|
||||
/tests/components/rflink/ @javicalle
|
||||
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||
@@ -1264,17 +1243,13 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/subaru/ @G-Two
|
||||
/tests/components/subaru/ @G-Two
|
||||
/homeassistant/components/suez_water/ @ooii
|
||||
/tests/components/suez_water/ @ooii
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/sunweg/ @rokam
|
||||
/tests/components/sunweg/ @rokam
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
/homeassistant/components/swiss_hydrological_data/ @fabaff
|
||||
/homeassistant/components/swiss_public_transport/ @fabaff @miaucl
|
||||
/tests/components/swiss_public_transport/ @fabaff @miaucl
|
||||
/homeassistant/components/swiss_public_transport/ @fabaff
|
||||
/homeassistant/components/switch/ @home-assistant/core
|
||||
/tests/components/switch/ @home-assistant/core
|
||||
/homeassistant/components/switch_as_x/ @home-assistant/core
|
||||
@@ -1297,16 +1272,12 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/synology_srm/ @aerialls
|
||||
/homeassistant/components/system_bridge/ @timmo001
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||
/tests/components/systemmonitor/ @gjohansson-ST
|
||||
/homeassistant/components/tado/ @michaelarnauts @chiefdragon @erwindouna
|
||||
/tests/components/tado/ @michaelarnauts @chiefdragon @erwindouna
|
||||
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
|
||||
/tests/components/tado/ @michaelarnauts @chiefdragon
|
||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||
/tests/components/tag/ @balloob @dmulcahey
|
||||
/homeassistant/components/tailscale/ @frenck
|
||||
/tests/components/tailscale/ @frenck
|
||||
/homeassistant/components/tailwind/ @frenck
|
||||
/tests/components/tailwind/ @frenck
|
||||
/homeassistant/components/tami4/ @Guy293
|
||||
/tests/components/tami4/ @Guy293
|
||||
/homeassistant/components/tankerkoenig/ @guillempages @mib1185
|
||||
@@ -1322,8 +1293,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||
/tests/components/tesla_wall_connector/ @einarhauks
|
||||
/homeassistant/components/tessie/ @Bre77
|
||||
/tests/components/tessie/ @Bre77
|
||||
/homeassistant/components/text/ @home-assistant/core
|
||||
/tests/components/text/ @home-assistant/core
|
||||
/homeassistant/components/tfiac/ @fredrike @mellado
|
||||
@@ -1391,7 +1360,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ukraine_alarm/ @PaulAnnekov
|
||||
/homeassistant/components/unifi/ @Kane610
|
||||
/tests/components/unifi/ @Kane610
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
|
||||
/tests/components/unifiprotect/ @AngellusMortis @bdraco
|
||||
@@ -1420,8 +1388,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vacuum/ @home-assistant/core
|
||||
/homeassistant/components/vallox/ @andre-richter @slovdahl @viiru-
|
||||
/tests/components/vallox/ @andre-richter @slovdahl @viiru-
|
||||
/homeassistant/components/valve/ @home-assistant/core
|
||||
/tests/components/valve/ @home-assistant/core
|
||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||
/tests/components/velbus/ @Cereal2nd @brefra
|
||||
/homeassistant/components/velux/ @Julius2342
|
||||
@@ -1430,8 +1396,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/versasense/ @imstevenxyz
|
||||
/homeassistant/components/version/ @ludeeus
|
||||
/tests/components/version/ @ludeeus
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey
|
||||
/homeassistant/components/vicare/ @CFenner
|
||||
/tests/components/vicare/ @CFenner
|
||||
/homeassistant/components/vilfo/ @ManneW
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
# Automatically generated by hassfest.
|
||||
#
|
||||
# To update, run python3 -m script.hassfest -p docker
|
||||
ARG BUILD_FROM
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
# Synchronize with homeassistant/core.py:async_stop
|
||||
ENV \
|
||||
S6_SERVICES_GRACETIME=240000
|
||||
S6_SERVICES_GRACETIME=220000
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ from .const import (
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity,
|
||||
entity_registry,
|
||||
@@ -474,9 +473,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||
"""Get domains of components to set up."""
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
domains = {
|
||||
domain for key in config if (domain := cv.domain_key(key)) != core.DOMAIN
|
||||
}
|
||||
domains = {key.partition(" ")[0] for key in config if key != core.DOMAIN}
|
||||
|
||||
# Add config entry domains
|
||||
if not hass.config.recovery_mode:
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "flexit",
|
||||
"name": "Flexit",
|
||||
"integrations": ["flexit", "flexit_bacnet"]
|
||||
}
|
||||
@@ -27,7 +27,7 @@ ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AbodeSensorDescriptionMixin:
|
||||
"""Mixin for Abode sensor."""
|
||||
|
||||
@@ -35,7 +35,7 @@ class AbodeSensorDescriptionMixin:
|
||||
native_unit_of_measurement_fn: Callable[[AbodeSense], str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin):
|
||||
"""Class describing Abode sensor entities."""
|
||||
|
||||
|
||||
@@ -45,14 +45,14 @@ from .const import (
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AccuWeatherSensorDescriptionMixin:
|
||||
"""Mixin for AccuWeather sensor."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], str | int | float | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AccuWeatherSensorDescription(
|
||||
SensorEntityDescription, AccuWeatherSensorDescriptionMixin
|
||||
):
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiopulse"],
|
||||
"requirements": ["aiopulse==0.4.4"]
|
||||
"requirements": ["aiopulse==0.4.3"]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
# convert title and unique_id to string
|
||||
if config_entry.version == 1:
|
||||
if isinstance(config_entry.unique_id, int):
|
||||
hass.config_entries.async_update_entry( # type: ignore[unreachable]
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
unique_id=str(config_entry.unique_id),
|
||||
title=str(config_entry.title),
|
||||
|
||||
@@ -137,7 +137,7 @@ class LocalAdaxDevice(ClimateEntity):
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None:
|
||||
def __init__(self, adax_data_handler, unique_id):
|
||||
"""Initialize the heater."""
|
||||
self._adax_data_handler = adax_data_handler
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
@@ -36,9 +36,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
@@ -61,9 +59,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_local()
|
||||
return await self.async_step_cloud()
|
||||
|
||||
async def async_step_local(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_local(self, user_input=None):
|
||||
"""Handle the local step."""
|
||||
data_schema = vol.Schema(
|
||||
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["adax", "adax_local"],
|
||||
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
|
||||
"requirements": ["adax==0.3.0", "Adax-local==0.1.5"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=300)
|
||||
PARALLEL_UPDATES = 4
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@dataclass(kw_only=True)
|
||||
class AdGuardHomeEntityDescription(SensorEntityDescription):
|
||||
"""Describes AdGuard Home sensor entity."""
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ SCAN_INTERVAL = timedelta(seconds=10)
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@dataclass(kw_only=True)
|
||||
class AdGuardHomeSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes AdGuard Home switch entity."""
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
|
||||
@@ -27,7 +26,6 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUEST_REFRESH_DELAY = 0.5
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -53,9 +51,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
name="Advantage Air",
|
||||
update_method=async_get,
|
||||
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||
),
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -21,7 +21,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||
ADVANTAGE_AIR_STATE_CLOSE,
|
||||
ADVANTAGE_AIR_STATE_OFF,
|
||||
ADVANTAGE_AIR_STATE_ON,
|
||||
@@ -40,6 +39,16 @@ ADVANTAGE_AIR_HVAC_MODES = {
|
||||
}
|
||||
HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()}
|
||||
|
||||
ADVANTAGE_AIR_FAN_MODES = {
|
||||
"autoAA": FAN_AUTO,
|
||||
"low": FAN_LOW,
|
||||
"medium": FAN_MEDIUM,
|
||||
"high": FAN_HIGH,
|
||||
}
|
||||
HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()}
|
||||
FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100}
|
||||
|
||||
ADVANTAGE_AIR_AUTOFAN = "aaAutoFanModeEnabled"
|
||||
ADVANTAGE_AIR_MYZONE = "MyZone"
|
||||
ADVANTAGE_AIR_MYAUTO = "MyAuto"
|
||||
ADVANTAGE_AIR_MYAUTO_ENABLED = "myAutoModeEnabled"
|
||||
@@ -47,7 +56,6 @@ ADVANTAGE_AIR_MYTEMP = "MyTemp"
|
||||
ADVANTAGE_AIR_MYTEMP_ENABLED = "climateControlModeEnabled"
|
||||
ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
|
||||
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
|
||||
ADVANTAGE_AIR_MYFAN = "autoAA"
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -77,25 +85,27 @@ async def async_setup_entry(
|
||||
class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
"""AdvantageAir AC unit."""
|
||||
|
||||
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_AUTO]
|
||||
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_max_temp = 32
|
||||
_attr_min_temp = 16
|
||||
_attr_name = None
|
||||
|
||||
_attr_hvac_modes = [
|
||||
HVACMode.OFF,
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.FAN_ONLY,
|
||||
HVACMode.DRY,
|
||||
]
|
||||
|
||||
_attr_supported_features = ClimateEntityFeature.FAN_MODE
|
||||
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
||||
"""Initialize an AdvantageAir AC unit."""
|
||||
super().__init__(instance, ac_key)
|
||||
|
||||
self._attr_supported_features = ClimateEntityFeature.FAN_MODE
|
||||
self._attr_hvac_modes = [
|
||||
HVACMode.OFF,
|
||||
HVACMode.COOL,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.FAN_ONLY,
|
||||
HVACMode.DRY,
|
||||
]
|
||||
# Set supported features and HVAC modes based on current operating mode
|
||||
if self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED):
|
||||
# MyAuto
|
||||
@@ -108,6 +118,10 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
# MyZone
|
||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
|
||||
# Add "ezfan" mode if supported
|
||||
if self._ac.get(ADVANTAGE_AIR_AUTOFAN):
|
||||
self._attr_fan_modes += [FAN_AUTO]
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the selected zones current temperature."""
|
||||
@@ -137,7 +151,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the current fan modes."""
|
||||
return FAN_AUTO if self._ac["fan"] == ADVANTAGE_AIR_MYFAN else self._ac["fan"]
|
||||
return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"])
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
@@ -175,11 +189,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set the Fan Mode."""
|
||||
if fan_mode == FAN_AUTO and self._ac.get(ADVANTAGE_AIR_AUTOFAN_ENABLED):
|
||||
mode = ADVANTAGE_AIR_MYFAN
|
||||
else:
|
||||
mode = fan_mode
|
||||
await self.async_update_ac({"fan": mode})
|
||||
await self.async_update_ac({"fan": HASS_FAN_MODES.get(fan_mode)})
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the Temperature."""
|
||||
|
||||
@@ -5,4 +5,3 @@ ADVANTAGE_AIR_STATE_OPEN = "open"
|
||||
ADVANTAGE_AIR_STATE_CLOSE = "close"
|
||||
ADVANTAGE_AIR_STATE_ON = "on"
|
||||
ADVANTAGE_AIR_STATE_OFF = "off"
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED = "aaAutoFanModeEnabled"
|
||||
|
||||
@@ -30,7 +30,7 @@ class AdvantageAirEntity(CoordinatorEntity):
|
||||
async def update_handle(*values):
|
||||
try:
|
||||
if await func(*keys, *values):
|
||||
await self.coordinator.async_request_refresh()
|
||||
await self.coordinator.async_refresh()
|
||||
except ApiError as err:
|
||||
raise HomeAssistantError(err) from err
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||
ADVANTAGE_AIR_STATE_OFF,
|
||||
ADVANTAGE_AIR_STATE_ON,
|
||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||
@@ -30,8 +29,6 @@ async def async_setup_entry(
|
||||
for ac_key, ac_device in aircons.items():
|
||||
if ac_device["info"]["freshAirStatus"] != "none":
|
||||
entities.append(AdvantageAirFreshAir(instance, ac_key))
|
||||
if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]:
|
||||
entities.append(AdvantageAirMyFan(instance, ac_key))
|
||||
if things := instance.coordinator.data.get("myThings"):
|
||||
for thing in things["things"].values():
|
||||
if thing["channelDipState"] == 8: # 8 = Other relay
|
||||
@@ -65,32 +62,6 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity):
|
||||
await self.async_update_ac({"freshAirStatus": ADVANTAGE_AIR_STATE_OFF})
|
||||
|
||||
|
||||
class AdvantageAirMyFan(AdvantageAirAcEntity, SwitchEntity):
|
||||
"""Representation of Advantage Air MyFan control."""
|
||||
|
||||
_attr_icon = "mdi:fan-auto"
|
||||
_attr_name = "MyFan"
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
|
||||
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
|
||||
"""Initialize an Advantage Air MyFan control."""
|
||||
super().__init__(instance, ac_key)
|
||||
self._attr_unique_id += "-myfan"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the MyFan status."""
|
||||
return self._ac[ADVANTAGE_AIR_AUTOFAN_ENABLED]
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn MyFan on."""
|
||||
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: True})
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn MyFan off."""
|
||||
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: False})
|
||||
|
||||
|
||||
class AdvantageAirRelay(AdvantageAirThingEntity, SwitchEntity):
|
||||
"""Representation of Advantage Air Thing."""
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Config flow for AEMET OpenData."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aemet_opendata.exceptions import AuthError
|
||||
from aemet_opendata.interface import AEMET, ConnectionOptions
|
||||
import voluptuous as vol
|
||||
@@ -10,7 +8,6 @@ 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
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaFlowFormStep,
|
||||
@@ -32,9 +29,7 @@ OPTIONS_FLOW = {
|
||||
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for AEMET OpenData."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.4.7"]
|
||||
"requirements": ["AEMET-OpenData==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Virtual integration: AEP Ohio."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "aepohio",
|
||||
"name": "AEP Ohio",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
"""Virtual integration: AEP Texas."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "aeptexas",
|
||||
"name": "AEP Texas",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
@@ -51,6 +51,25 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
|
||||
"""Import configuration from yaml."""
|
||||
try:
|
||||
self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]})
|
||||
except AbortFlow as err:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_issue_already_configured",
|
||||
breaks_in_ha_version="2024.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue_already_configured",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "AfterShip",
|
||||
},
|
||||
)
|
||||
raise err
|
||||
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
@@ -65,8 +84,6 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"integration_title": "AfterShip",
|
||||
},
|
||||
)
|
||||
|
||||
self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]})
|
||||
return self.async_create_entry(
|
||||
title=config.get(CONF_NAME, "AfterShip"),
|
||||
data={CONF_API_KEY: config[CONF_API_KEY]},
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_already_configured": {
|
||||
"title": "The {integration_title} YAML configuration import failed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but the YAML configuration was already imported.\n\nRemove the YAML configuration and restart Home Assistant."
|
||||
},
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"title": "The {integration_title} YAML configuration import failed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Config flow to configure Agent devices."""
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
from agent import AgentConnectionError, AgentError
|
||||
from agent.a import Agent
|
||||
@@ -8,7 +7,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, SERVER_URL
|
||||
@@ -20,9 +18,11 @@ DEFAULT_PORT = 8090
|
||||
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle an Agent config flow."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
def __init__(self):
|
||||
"""Initialize the Agent config flow."""
|
||||
self.device_config = {}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle an Agent config flow."""
|
||||
errors = {}
|
||||
|
||||
@@ -49,15 +49,13 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
device_config = {
|
||||
self.device_config = {
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
SERVER_URL: server_origin,
|
||||
}
|
||||
|
||||
return self.async_create_entry(
|
||||
title=agent_client.name, data=device_config
|
||||
)
|
||||
return await self._create_entry(agent_client.name)
|
||||
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
@@ -68,6 +66,11 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
description_placeholders=self.device_config,
|
||||
data_schema=vol.Schema(data),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _create_entry(self, server_name):
|
||||
"""Create entry for device."""
|
||||
return self.async_create_entry(title=server_name, data=self.device_config)
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of the Agent DVR server."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ from .const import (
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirlySensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Airly sensor entities."""
|
||||
|
||||
|
||||
@@ -6,17 +6,8 @@ from pyairnow import WebServiceAPI
|
||||
from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -25,7 +16,7 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
@@ -55,14 +46,12 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for AirNow."""
|
||||
|
||||
VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
@@ -119,18 +108,18 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@staticmethod
|
||||
@core.callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlow:
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> config_entries.OptionsFlow:
|
||||
"""Return the options flow."""
|
||||
return AirNowOptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
|
||||
"""Handle an options flow for AirNow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data=user_input)
|
||||
@@ -152,13 +141,13 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class InvalidLocation(HomeAssistantError):
|
||||
class InvalidLocation(exceptions.HomeAssistantError):
|
||||
"""Error to indicate the location is invalid."""
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
"""DataUpdateCoordinator for the AirNow integration."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from pyairnow import WebServiceAPI
|
||||
from pyairnow.conv import aqi_to_concentration
|
||||
from pyairnow.errors import AirNowError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
@@ -35,19 +31,12 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""The AirNow update coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
session: ClientSession,
|
||||
api_key: str,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
distance: int,
|
||||
update_interval: timedelta,
|
||||
) -> None:
|
||||
self, hass, session, api_key, latitude, longitude, distance, update_interval
|
||||
):
|
||||
"""Initialize."""
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
@@ -57,7 +46,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
async def _async_update_data(self):
|
||||
"""Update data via library."""
|
||||
data = {}
|
||||
try:
|
||||
|
||||
@@ -51,7 +51,7 @@ ATTR_LEVEL = "level"
|
||||
ATTR_STATION = "reporting_station"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirNowEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
@@ -59,7 +59,7 @@ class AirNowEntityDescriptionMixin:
|
||||
extra_state_attributes_fn: Callable[[Any], dict[str, str]] | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMixin):
|
||||
"""Describes Airnow sensor entity."""
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairq import AirQ, InvalidAuth
|
||||
from aioairq import AirQ, InvalidAuth, InvalidInput
|
||||
from aiohttp.client_exceptions import ClientConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -42,32 +42,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
|
||||
try:
|
||||
await airq.validate()
|
||||
except ClientConnectionError:
|
||||
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
|
||||
except InvalidInput:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Failed to connect to device %s. Check the IP address / device"
|
||||
" ID as well as whether the device is connected to power and"
|
||||
" the WiFi"
|
||||
),
|
||||
"%s does not appear to be a valid IP address or mDNS name",
|
||||
user_input[CONF_IP_ADDRESS],
|
||||
)
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
_LOGGER.debug(
|
||||
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
|
||||
)
|
||||
errors["base"] = "invalid_auth"
|
||||
errors["base"] = "invalid_input"
|
||||
else:
|
||||
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
||||
try:
|
||||
await airq.validate()
|
||||
except ClientConnectionError:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Failed to connect to device %s. Check the IP address / device"
|
||||
" ID as well as whether the device is connected to power and"
|
||||
" the WiFi"
|
||||
),
|
||||
user_input[CONF_IP_ADDRESS],
|
||||
)
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
_LOGGER.debug(
|
||||
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
|
||||
)
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Successfully connected to %s", user_input[CONF_IP_ADDRESS]
|
||||
)
|
||||
|
||||
device_info = await airq.fetch_device_info()
|
||||
await self.async_set_unique_id(device_info["id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
device_info = await airq.fetch_device_info()
|
||||
await self.async_set_unique_id(device_info["id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=device_info["name"], data=user_input)
|
||||
return self.async_create_entry(
|
||||
title=device_info["name"], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Final
|
||||
|
||||
DOMAIN: Final = "airq"
|
||||
MANUFACTURER: Final = "CorantGmbH"
|
||||
TARGET_ROUTE: Final = "average"
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
|
||||
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
|
||||
UPDATE_INTERVAL: float = 10.0
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, UPDATE_INTERVAL
|
||||
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,4 +56,6 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
hw_version=info["hw_version"],
|
||||
)
|
||||
)
|
||||
return await self.airq.get_latest_data()
|
||||
|
||||
data = await self.airq.get(TARGET_ROUTE)
|
||||
return self.airq.drop_uncertainties_from_data(data)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.3.2"]
|
||||
"requirements": ["aioairq==0.2.4"]
|
||||
}
|
||||
|
||||
@@ -37,14 +37,14 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirQEntityDescriptionMixin:
|
||||
"""Class for keys required by AirQ entity."""
|
||||
|
||||
value: Callable[[dict], float | int | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin):
|
||||
"""Describes AirQ sensor entity."""
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ import logging
|
||||
from airthings import Airthings, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SECRET, DOMAIN
|
||||
from .const import CONF_ID, CONF_SECRET, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -8,11 +8,10 @@ import airthings
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_SECRET, DOMAIN
|
||||
from .const import CONF_ID, CONF_SECRET, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
DOMAIN = "airthings"
|
||||
|
||||
CONF_ID = "id"
|
||||
CONF_SECRET = "secret"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for airthings ble sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
|
||||
from airthings_ble import AirthingsDevice
|
||||
@@ -168,13 +167,10 @@ async def async_setup_entry(
|
||||
# we need to change some units
|
||||
sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy()
|
||||
if not is_metric:
|
||||
for key, val in sensors_mapping.items():
|
||||
for val in sensors_mapping.values():
|
||||
if val.native_unit_of_measurement is not VOLUME_BECQUEREL:
|
||||
continue
|
||||
sensors_mapping[key] = dataclasses.replace(
|
||||
val,
|
||||
native_unit_of_measurement=VOLUME_PICOCURIE,
|
||||
)
|
||||
val.native_unit_of_measurement = VOLUME_PICOCURIE
|
||||
|
||||
entities = []
|
||||
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components import automation
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_COUNTRY,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
@@ -45,6 +44,7 @@ from homeassistant.helpers.update_coordinator import (
|
||||
|
||||
from .const import (
|
||||
CONF_CITY,
|
||||
CONF_COUNTRY,
|
||||
CONF_GEOGRAPHIES,
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DOMAIN,
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_COUNTRY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_SHOW_ON_MAP,
|
||||
@@ -36,6 +35,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
from . import async_get_geography_id
|
||||
from .const import (
|
||||
CONF_CITY,
|
||||
CONF_COUNTRY,
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
|
||||
@@ -9,5 +9,6 @@ INTEGRATION_TYPE_GEOGRAPHY_NAME = "Geographical Location by Name"
|
||||
INTEGRATION_TYPE_NODE_PRO = "AirVisual Node/Pro"
|
||||
|
||||
CONF_CITY = "city"
|
||||
CONF_COUNTRY = "country"
|
||||
CONF_GEOGRAPHIES = "geographies"
|
||||
CONF_INTEGRATION_TYPE = "integration_type"
|
||||
|
||||
@@ -7,7 +7,6 @@ from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_COUNTRY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_STATE,
|
||||
@@ -16,7 +15,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import CONF_CITY, DOMAIN
|
||||
from .const import CONF_CITY, CONF_COUNTRY, DOMAIN
|
||||
|
||||
CONF_COORDINATES = "coordinates"
|
||||
CONF_TITLE = "title"
|
||||
|
||||
@@ -15,7 +15,6 @@ from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONF_COUNTRY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_SHOW_ON_MAP,
|
||||
@@ -26,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import AirVisualEntity
|
||||
from .const import CONF_CITY, DOMAIN
|
||||
from .const import CONF_CITY, CONF_COUNTRY, DOMAIN
|
||||
|
||||
ATTR_CITY = "city"
|
||||
ATTR_COUNTRY = "country"
|
||||
|
||||
@@ -26,7 +26,7 @@ from . import AirVisualProData, AirVisualProEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirVisualProMeasurementKeyMixin:
|
||||
"""Define an entity description mixin to include a measurement key."""
|
||||
|
||||
@@ -35,7 +35,7 @@ class AirVisualProMeasurementKeyMixin:
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirVisualProMeasurementDescription(
|
||||
SensorEntityDescription, AirVisualProMeasurementKeyMixin
|
||||
):
|
||||
|
||||
@@ -29,7 +29,7 @@ from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""A class that describes airzone binary sensor entities."""
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX)
|
||||
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN)
|
||||
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
||||
if self.supported_features & ClimateEntityFeature.FAN_MODE:
|
||||
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
||||
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||
@@ -257,5 +258,3 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
self._attr_target_temperature_low = self.get_airzone_value(
|
||||
AZD_HEAT_TEMP_SET
|
||||
)
|
||||
else:
|
||||
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.7.2"]
|
||||
"requirements": ["aioairzone==0.6.9"]
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirzoneSelectDescriptionMixin:
|
||||
"""Define an entity description mixin for select entities."""
|
||||
|
||||
@@ -34,7 +34,7 @@ class AirzoneSelectDescriptionMixin:
|
||||
options_dict: dict[str, int]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescriptionMixin):
|
||||
"""Class to describe an Airzone select entity."""
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from .entity import (
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""A class that describes Airzone Cloud binary sensor entities."""
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ 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 HomeAssistantError, PlatformNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@@ -34,34 +33,6 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
(AladdinDevice(acc, door, config_entry) for door in doors),
|
||||
)
|
||||
remove_stale_devices(hass, config_entry, doors)
|
||||
|
||||
|
||||
def remove_stale_devices(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, devices: list[dict]
|
||||
) -> None:
|
||||
"""Remove stale devices from device registry."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
all_device_ids = {f"{door['device_id']}-{door['door_number']}" for door in devices}
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
|
||||
for identifier in device_entry.identifiers:
|
||||
if identifier[0] == DOMAIN:
|
||||
device_id = identifier[1]
|
||||
break
|
||||
|
||||
if device_id is None or device_id not in all_device_ids:
|
||||
# If device_id is None an invalid device entry was found for this config entry.
|
||||
# If the device_id is not in existing device ids it's a stale device entry.
|
||||
# Remove config entry from this device entry in either case.
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
class AladdinDevice(CoverEntity):
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["AIOAladdinConnect==0.1.58"]
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ from .const import DOMAIN
|
||||
from .model import DoorDevice
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AccSensorEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AccSensorEntityDescription(
|
||||
SensorEntityDescription, AccSensorEntityDescriptionMixin
|
||||
):
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Component to interface with an alarm control panel."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Final, final
|
||||
from typing import Any, Final, final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -23,41 +23,26 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import make_entity_service_schema
|
||||
from homeassistant.helpers.deprecation import (
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
_DEPRECATED_FORMAT_NUMBER,
|
||||
_DEPRECATED_FORMAT_TEXT,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_HOME,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION,
|
||||
_DEPRECATED_SUPPORT_ALARM_TRIGGER,
|
||||
ATTR_CHANGED_BY,
|
||||
ATTR_CODE_ARM_REQUIRED,
|
||||
DOMAIN,
|
||||
FORMAT_NUMBER,
|
||||
FORMAT_TEXT,
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
SUPPORT_ALARM_ARM_VACATION,
|
||||
SUPPORT_ALARM_TRIGGER,
|
||||
AlarmControlPanelEntityFeature,
|
||||
CodeFormat,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from functools import cached_property
|
||||
else:
|
||||
from homeassistant.backports.functools import cached_property
|
||||
|
||||
# As we import constants of the cost module here, we need to add the following
|
||||
# functions to check for deprecated constants again
|
||||
# Both can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(dir_with_deprecated_constants, module_globals=globals())
|
||||
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||
@@ -136,19 +121,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return await component.async_unload_entry(entry)
|
||||
|
||||
|
||||
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
@dataclass
|
||||
class AlarmControlPanelEntityDescription(EntityDescription):
|
||||
"""A class that describes alarm control panel entities."""
|
||||
|
||||
|
||||
CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||
"code_format",
|
||||
"changed_by",
|
||||
"code_arm_required",
|
||||
"supported_features",
|
||||
}
|
||||
|
||||
|
||||
class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
class AlarmControlPanelEntity(Entity):
|
||||
"""An abstract class for alarm control entities."""
|
||||
|
||||
entity_description: AlarmControlPanelEntityDescription
|
||||
@@ -159,17 +137,17 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
AlarmControlPanelEntityFeature(0)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def code_format(self) -> CodeFormat | None:
|
||||
"""Code format or None if no code is required."""
|
||||
return self._attr_code_format
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def changed_by(self) -> str | None:
|
||||
"""Last change triggered by."""
|
||||
return self._attr_changed_by
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def code_arm_required(self) -> bool:
|
||||
"""Whether the code is required for arm actions."""
|
||||
return self._attr_code_arm_required
|
||||
@@ -230,7 +208,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
"""Send arm custom bypass command."""
|
||||
await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def supported_features(self) -> AlarmControlPanelEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
"""Provides the constants needed for component."""
|
||||
from enum import IntFlag, StrEnum
|
||||
from functools import partial
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
|
||||
DOMAIN: Final = "alarm_control_panel"
|
||||
|
||||
ATTR_CHANGED_BY: Final = "changed_by"
|
||||
@@ -22,10 +15,10 @@ class CodeFormat(StrEnum):
|
||||
NUMBER = "number"
|
||||
|
||||
|
||||
# These constants are deprecated as of Home Assistant 2022.5, can be removed in 2025.1
|
||||
# These constants are deprecated as of Home Assistant 2022.5
|
||||
# Please use the CodeFormat enum instead.
|
||||
_DEPRECATED_FORMAT_TEXT: Final = DeprecatedConstantEnum(CodeFormat.TEXT, "2025.1")
|
||||
_DEPRECATED_FORMAT_NUMBER: Final = DeprecatedConstantEnum(CodeFormat.NUMBER, "2025.1")
|
||||
FORMAT_TEXT: Final = "text"
|
||||
FORMAT_NUMBER: Final = "number"
|
||||
|
||||
|
||||
class AlarmControlPanelEntityFeature(IntFlag):
|
||||
@@ -41,28 +34,12 @@ class AlarmControlPanelEntityFeature(IntFlag):
|
||||
|
||||
# These constants are deprecated as of Home Assistant 2022.5
|
||||
# Please use the AlarmControlPanelEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_HOME: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_HOME, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_AWAY, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_NIGHT, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_TRIGGER: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.TRIGGER, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION, "2025.1"
|
||||
)
|
||||
|
||||
# Both can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(dir_with_deprecated_constants, module_globals=globals())
|
||||
SUPPORT_ALARM_ARM_HOME: Final = 1
|
||||
SUPPORT_ALARM_ARM_AWAY: Final = 2
|
||||
SUPPORT_ALARM_ARM_NIGHT: Final = 4
|
||||
SUPPORT_ALARM_TRIGGER: Final = 8
|
||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = 16
|
||||
SUPPORT_ALARM_ARM_VACATION: Final = 32
|
||||
|
||||
CONDITION_TRIGGERED: Final = "is_triggered"
|
||||
CONDITION_DISARMED: Final = "is_disarmed"
|
||||
|
||||
@@ -28,7 +28,13 @@ from homeassistant.helpers.entity import get_supported_features
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import ATTR_CODE_ARM_REQUIRED, DOMAIN
|
||||
from .const import AlarmControlPanelEntityFeature
|
||||
from .const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
SUPPORT_ALARM_ARM_VACATION,
|
||||
SUPPORT_ALARM_TRIGGER,
|
||||
)
|
||||
|
||||
ACTION_TYPES: Final[set[str]] = {
|
||||
"arm_away",
|
||||
@@ -76,16 +82,16 @@ async def async_get_actions(
|
||||
}
|
||||
|
||||
# Add actions for each entity that belongs to this integration
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
|
||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
||||
actions.append({**base_action, CONF_TYPE: "arm_away"})
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
|
||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
||||
actions.append({**base_action, CONF_TYPE: "arm_home"})
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
|
||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
||||
actions.append({**base_action, CONF_TYPE: "arm_night"})
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
|
||||
if supported_features & SUPPORT_ALARM_ARM_VACATION:
|
||||
actions.append({**base_action, CONF_TYPE: "arm_vacation"})
|
||||
actions.append({**base_action, CONF_TYPE: "disarm"})
|
||||
if supported_features & AlarmControlPanelEntityFeature.TRIGGER:
|
||||
if supported_features & SUPPORT_ALARM_TRIGGER:
|
||||
actions.append({**base_action, CONF_TYPE: "trigger"})
|
||||
|
||||
return actions
|
||||
|
||||
@@ -39,7 +39,11 @@ from .const import (
|
||||
CONDITION_ARMED_VACATION,
|
||||
CONDITION_DISARMED,
|
||||
CONDITION_TRIGGERED,
|
||||
AlarmControlPanelEntityFeature,
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
SUPPORT_ALARM_ARM_VACATION,
|
||||
)
|
||||
|
||||
CONDITION_TYPES: Final[set[str]] = {
|
||||
@@ -86,15 +90,15 @@ async def async_get_conditions(
|
||||
{**base_condition, CONF_TYPE: CONDITION_DISARMED},
|
||||
{**base_condition, CONF_TYPE: CONDITION_TRIGGERED},
|
||||
]
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
|
||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_HOME})
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
|
||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_AWAY})
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
|
||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_NIGHT})
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
|
||||
if supported_features & SUPPORT_ALARM_ARM_VACATION:
|
||||
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_VACATION})
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS:
|
||||
if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS:
|
||||
conditions.append(
|
||||
{**base_condition, CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS}
|
||||
)
|
||||
|
||||
@@ -29,7 +29,12 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
from .const import AlarmControlPanelEntityFeature
|
||||
from .const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
SUPPORT_ALARM_ARM_VACATION,
|
||||
)
|
||||
|
||||
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
|
||||
TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | {
|
||||
@@ -77,28 +82,28 @@ async def async_get_triggers(
|
||||
}
|
||||
for trigger in BASIC_TRIGGER_TYPES
|
||||
]
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
|
||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
||||
triggers.append(
|
||||
{
|
||||
**base_trigger,
|
||||
CONF_TYPE: "armed_home",
|
||||
}
|
||||
)
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
|
||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
||||
triggers.append(
|
||||
{
|
||||
**base_trigger,
|
||||
CONF_TYPE: "armed_away",
|
||||
}
|
||||
)
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
|
||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
||||
triggers.append(
|
||||
{
|
||||
**base_trigger,
|
||||
CONF_TYPE: "armed_night",
|
||||
}
|
||||
)
|
||||
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
|
||||
if supported_features & SUPPORT_ALARM_ARM_VACATION:
|
||||
triggers.append(
|
||||
{
|
||||
**base_trigger,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"""Helper to test significant Alarm Control Panel state changes."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_CHANGED_BY, ATTR_CODE_ARM_REQUIRED
|
||||
|
||||
SIGNIFICANT_ATTRIBUTES: set[str] = {
|
||||
ATTR_CHANGED_BY,
|
||||
ATTR_CODE_ARM_REQUIRED,
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_check_significant_change(
|
||||
hass: HomeAssistant,
|
||||
old_state: str,
|
||||
old_attrs: dict,
|
||||
new_state: str,
|
||||
new_attrs: dict,
|
||||
**kwargs: Any,
|
||||
) -> bool | None:
|
||||
"""Test if state significantly changed."""
|
||||
if old_state != new_state:
|
||||
return True
|
||||
|
||||
old_attrs_s = set(
|
||||
{k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items()
|
||||
)
|
||||
new_attrs_s = set(
|
||||
{k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items()
|
||||
)
|
||||
changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s}
|
||||
|
||||
if changed_attrs:
|
||||
return True
|
||||
|
||||
# no significant attribute change detected
|
||||
return False
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from adext import AdExt
|
||||
from alarmdecoder.devices import SerialDevice, SocketDevice
|
||||
@@ -13,10 +12,8 @@ from homeassistant import config_entries
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import (
|
||||
CONF_ALT_NIGHT_MODE,
|
||||
@@ -69,9 +66,7 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Get the options flow for AlarmDecoder."""
|
||||
return AlarmDecoderOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
self.protocol = user_input[CONF_PROTOCOL]
|
||||
@@ -88,9 +83,7 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_protocol(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_protocol(self, user_input=None):
|
||||
"""Handle AlarmDecoder protocol setup."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
@@ -153,18 +146,15 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle AlarmDecoder options."""
|
||||
|
||||
selected_zone: str | None = None
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize AlarmDecoder options flow."""
|
||||
self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
|
||||
self.zone_options = config_entry.options.get(
|
||||
OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS
|
||||
)
|
||||
self.selected_zone = None
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
if user_input[EDIT_KEY] == EDIT_SETTINGS:
|
||||
@@ -183,9 +173,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_arm_settings(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_arm_settings(self, user_input=None):
|
||||
"""Arming options form."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
@@ -212,9 +200,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_zone_select(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_zone_select(self, user_input=None):
|
||||
"""Zone selection form."""
|
||||
errors = _validate_zone_input(user_input)
|
||||
|
||||
@@ -230,9 +216,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_zone_details(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_zone_details(self, user_input=None):
|
||||
"""Zone details form."""
|
||||
errors = _validate_zone_input(user_input)
|
||||
|
||||
@@ -309,7 +293,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
)
|
||||
|
||||
|
||||
def _validate_zone_input(zone_input: dict[str, Any] | None) -> dict[str, str]:
|
||||
def _validate_zone_input(zone_input):
|
||||
if not zone_input:
|
||||
return {}
|
||||
errors = {}
|
||||
@@ -343,7 +327,7 @@ def _validate_zone_input(zone_input: dict[str, Any] | None) -> dict[str, str]:
|
||||
return errors
|
||||
|
||||
|
||||
def _fix_input_types(zone_input: dict[str, Any]) -> dict[str, Any]:
|
||||
def _fix_input_types(zone_input):
|
||||
"""Convert necessary keys to int.
|
||||
|
||||
Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
|
||||
@@ -357,9 +341,7 @@ def _fix_input_types(zone_input: dict[str, Any]) -> dict[str, Any]:
|
||||
return zone_input
|
||||
|
||||
|
||||
def _device_already_added(
|
||||
current_entries: list[ConfigEntry], user_input: dict[str, Any], protocol: str | None
|
||||
) -> bool:
|
||||
def _device_already_added(current_entries, user_input, protocol):
|
||||
"""Determine if entry has already been added to HA."""
|
||||
user_host = user_input.get(CONF_HOST)
|
||||
user_port = user_input.get(CONF_PORT)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"device_path": "Device Path"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
|
||||
"host": "The hostname or IP address of AlarDecoder device that is connected to your alarm panel.",
|
||||
"port": "The port on which AlarmDecoder is accessible (for example, 10000)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,15 +36,6 @@ CONF_FLASH_BRIEFINGS = "flash_briefings"
|
||||
CONF_SMART_HOME = "smart_home"
|
||||
DEFAULT_LOCALE = "en-US"
|
||||
|
||||
# Alexa Smart Home API send events gateway endpoints
|
||||
# https://developer.amazon.com/en-US/docs/alexa/smarthome/send-events.html#endpoints
|
||||
VALID_ENDPOINTS = [
|
||||
"https://api.amazonalexa.com/v3/events",
|
||||
"https://api.eu.amazonalexa.com/v3/events",
|
||||
"https://api.fe.amazonalexa.com/v3/events",
|
||||
]
|
||||
|
||||
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
@@ -55,7 +46,7 @@ ALEXA_ENTITY_SCHEMA = vol.Schema(
|
||||
|
||||
SMART_HOME_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENDPOINT): vol.All(vol.Lower, vol.In(VALID_ENDPOINTS)),
|
||||
vol.Optional(CONF_ENDPOINT): cv.string,
|
||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||
vol.Optional(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_LOCALE, default=DEFAULT_LOCALE): vol.In(
|
||||
|
||||
@@ -19,8 +19,6 @@ from homeassistant.components import (
|
||||
number,
|
||||
timer,
|
||||
vacuum,
|
||||
valve,
|
||||
water_heater,
|
||||
)
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
@@ -437,8 +435,7 @@ class AlexaPowerController(AlexaCapability):
|
||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||
elif self.entity.domain == timer.DOMAIN:
|
||||
is_on = self.entity.state != STATE_IDLE
|
||||
elif self.entity.domain == water_heater.DOMAIN:
|
||||
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
||||
|
||||
else:
|
||||
is_on = self.entity.state != STATE_OFF
|
||||
|
||||
@@ -941,9 +938,6 @@ class AlexaTemperatureSensor(AlexaCapability):
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)
|
||||
elif self.entity.domain == water_heater.DOMAIN:
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
temp = self.entity.attributes.get(water_heater.ATTR_CURRENT_TEMPERATURE)
|
||||
|
||||
if temp is None or temp in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return None
|
||||
@@ -1114,8 +1108,6 @@ class AlexaThermostatController(AlexaCapability):
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE:
|
||||
properties.append({"name": "targetSetpoint"})
|
||||
if supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE:
|
||||
properties.append({"name": "targetSetpoint"})
|
||||
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||
properties.append({"name": "lowerSetpoint"})
|
||||
properties.append({"name": "upperSetpoint"})
|
||||
@@ -1135,8 +1127,6 @@ class AlexaThermostatController(AlexaCapability):
|
||||
return None
|
||||
|
||||
if name == "thermostatMode":
|
||||
if self.entity.domain == water_heater.DOMAIN:
|
||||
return None
|
||||
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
|
||||
|
||||
mode: dict[str, str] | str | None
|
||||
@@ -1186,13 +1176,9 @@ class AlexaThermostatController(AlexaCapability):
|
||||
ThermostatMode Values.
|
||||
|
||||
ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM.
|
||||
Water heater devices do not return thermostat modes.
|
||||
"""
|
||||
if self.entity.domain == water_heater.DOMAIN:
|
||||
return None
|
||||
|
||||
supported_modes: list[str] = []
|
||||
hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES, [])
|
||||
hvac_modes = self.entity.attributes[climate.ATTR_HVAC_MODES]
|
||||
for mode in hvac_modes:
|
||||
if thermostat_mode := API_THERMOSTAT_MODES.get(mode):
|
||||
supported_modes.append(thermostat_mode)
|
||||
@@ -1422,16 +1408,6 @@ class AlexaModeController(AlexaCapability):
|
||||
if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []):
|
||||
return f"{humidifier.ATTR_MODE}.{mode}"
|
||||
|
||||
# Water heater operation mode
|
||||
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||
operation_mode = self.entity.attributes.get(
|
||||
water_heater.ATTR_OPERATION_MODE, None
|
||||
)
|
||||
if operation_mode in self.entity.attributes.get(
|
||||
water_heater.ATTR_OPERATION_LIST, []
|
||||
):
|
||||
return f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}"
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
# Return state instead of position when using ModeController.
|
||||
@@ -1445,19 +1421,6 @@ class AlexaModeController(AlexaCapability):
|
||||
):
|
||||
return f"{cover.ATTR_POSITION}.{mode}"
|
||||
|
||||
# Valve position state
|
||||
if self.instance == f"{valve.DOMAIN}.state":
|
||||
# Return state instead of position when using ModeController.
|
||||
state = self.entity.state
|
||||
if state in (
|
||||
valve.STATE_OPEN,
|
||||
valve.STATE_OPENING,
|
||||
valve.STATE_CLOSED,
|
||||
valve.STATE_CLOSING,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
return f"state.{state}"
|
||||
|
||||
return None
|
||||
|
||||
def configuration(self) -> dict[str, Any] | None:
|
||||
@@ -1515,26 +1478,6 @@ class AlexaModeController(AlexaCapability):
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Water heater operation modes
|
||||
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
|
||||
operation_modes = self.entity.attributes.get(
|
||||
water_heater.ATTR_OPERATION_LIST, []
|
||||
)
|
||||
for operation_mode in operation_modes:
|
||||
self._resource.add_mode(
|
||||
f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}",
|
||||
[operation_mode],
|
||||
)
|
||||
# Devices with a single mode completely break Alexa discovery,
|
||||
# add a fake preset (see issue #53832).
|
||||
if len(operation_modes) == 1:
|
||||
self._resource.add_mode(
|
||||
f"{water_heater.ATTR_OPERATION_MODE}.{PRESET_MODE_NA}",
|
||||
[PRESET_MODE_NA],
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
self._resource = AlexaModeResource(
|
||||
@@ -1554,32 +1497,6 @@ class AlexaModeController(AlexaCapability):
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Valve position resources
|
||||
if self.instance == f"{valve.DOMAIN}.state":
|
||||
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
self._resource = AlexaModeResource(
|
||||
["Preset", AlexaGlobalCatalog.SETTING_PRESET], False
|
||||
)
|
||||
modes = 0
|
||||
if supported_features & valve.ValveEntityFeature.OPEN:
|
||||
self._resource.add_mode(
|
||||
f"state.{valve.STATE_OPEN}",
|
||||
["Open", AlexaGlobalCatalog.SETTING_PRESET],
|
||||
)
|
||||
modes += 1
|
||||
if supported_features & valve.ValveEntityFeature.CLOSE:
|
||||
self._resource.add_mode(
|
||||
f"state.{valve.STATE_CLOSED}",
|
||||
["Closed", AlexaGlobalCatalog.SETTING_PRESET],
|
||||
)
|
||||
modes += 1
|
||||
|
||||
# Alexa requiers at least 2 modes
|
||||
if modes == 1:
|
||||
self._resource.add_mode(f"state.{PRESET_MODE_NA}", [PRESET_MODE_NA])
|
||||
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return {}
|
||||
|
||||
def semantics(self) -> dict[str, Any] | None:
|
||||
@@ -1618,34 +1535,6 @@ class AlexaModeController(AlexaCapability):
|
||||
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
# Valve Position
|
||||
if self.instance == f"{valve.DOMAIN}.state":
|
||||
close_labels = [AlexaSemantics.ACTION_CLOSE]
|
||||
open_labels = [AlexaSemantics.ACTION_OPEN]
|
||||
self._semantics = AlexaSemantics()
|
||||
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_CLOSED],
|
||||
f"state.{valve.STATE_CLOSED}",
|
||||
)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_OPEN],
|
||||
f"state.{valve.STATE_OPEN}",
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
close_labels,
|
||||
"SetMode",
|
||||
{"mode": f"state.{valve.STATE_CLOSED}"},
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
open_labels,
|
||||
"SetMode",
|
||||
{"mode": f"state.{valve.STATE_OPEN}"},
|
||||
)
|
||||
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -1759,10 +1648,6 @@ class AlexaRangeController(AlexaCapability):
|
||||
)
|
||||
return speed_index
|
||||
|
||||
# Valve Position
|
||||
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||
return self.entity.attributes.get(valve.ATTR_CURRENT_POSITION)
|
||||
|
||||
return None
|
||||
|
||||
def configuration(self) -> dict[str, Any] | None:
|
||||
@@ -1886,17 +1771,6 @@ class AlexaRangeController(AlexaCapability):
|
||||
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Valve Position Resources
|
||||
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||
self._resource = AlexaPresetResource(
|
||||
["Opening", AlexaGlobalCatalog.SETTING_OPENING],
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
precision=1,
|
||||
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return {}
|
||||
|
||||
def semantics(self) -> dict[str, Any] | None:
|
||||
@@ -1973,25 +1847,6 @@ class AlexaRangeController(AlexaCapability):
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
# Valve Position
|
||||
if self.instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||
close_labels = [AlexaSemantics.ACTION_CLOSE]
|
||||
open_labels = [AlexaSemantics.ACTION_OPEN]
|
||||
self._semantics = AlexaSemantics()
|
||||
|
||||
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||
self._semantics.add_states_to_range(
|
||||
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
close_labels, "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
open_labels, "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -2065,10 +1920,6 @@ class AlexaToggleController(AlexaCapability):
|
||||
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
|
||||
return "ON" if is_on else "OFF"
|
||||
|
||||
# Stop Valve
|
||||
if self.instance == f"{valve.DOMAIN}.stop":
|
||||
return "OFF"
|
||||
|
||||
return None
|
||||
|
||||
def capability_resources(self) -> dict[str, list[dict[str, Any]]]:
|
||||
@@ -2081,10 +1932,6 @@ class AlexaToggleController(AlexaCapability):
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
if self.instance == f"{valve.DOMAIN}.stop":
|
||||
self._resource = AlexaCapabilityResource(["Stop"])
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ from homeassistant.components import (
|
||||
switch,
|
||||
timer,
|
||||
vacuum,
|
||||
valve,
|
||||
water_heater,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@@ -250,9 +248,6 @@ class DisplayCategory:
|
||||
# Indicates a vacuum cleaner.
|
||||
VACUUM_CLEANER = "VACUUM_CLEANER"
|
||||
|
||||
# Indicates a water heater.
|
||||
WATER_HEATER = "WATER_HEATER"
|
||||
|
||||
# Indicates a network-connected wearable device, such as an Apple Watch,
|
||||
# Fitbit, or Samsung Gear.
|
||||
WEARABLE = "WEARABLE"
|
||||
@@ -461,46 +456,23 @@ class ButtonCapabilities(AlexaEntity):
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(water_heater.DOMAIN)
|
||||
class ClimateCapabilities(AlexaEntity):
|
||||
"""Class to represent Climate capabilities."""
|
||||
|
||||
def default_display_categories(self) -> list[str]:
|
||||
"""Return the display categories for this entity."""
|
||||
if self.entity.domain == water_heater.DOMAIN:
|
||||
return [DisplayCategory.WATER_HEATER]
|
||||
return [DisplayCategory.THERMOSTAT]
|
||||
|
||||
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||
"""Yield the supported interfaces."""
|
||||
# If we support two modes, one being off, we allow turning on too.
|
||||
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (
|
||||
self.entity.domain == climate.DOMAIN
|
||||
and climate.HVACMode.OFF
|
||||
in self.entity.attributes.get(climate.ATTR_HVAC_MODES, [])
|
||||
or self.entity.domain == water_heater.DOMAIN
|
||||
and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF)
|
||||
if climate.HVACMode.OFF in self.entity.attributes.get(
|
||||
climate.ATTR_HVAC_MODES, []
|
||||
):
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
if (
|
||||
self.entity.domain == climate.DOMAIN
|
||||
or self.entity.domain == water_heater.DOMAIN
|
||||
and (
|
||||
supported_features
|
||||
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
):
|
||||
yield AlexaThermostatController(self.hass, self.entity)
|
||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||
if self.entity.domain == water_heater.DOMAIN and (
|
||||
supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
||||
):
|
||||
yield AlexaModeController(
|
||||
self.entity,
|
||||
instance=f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}",
|
||||
)
|
||||
yield AlexaThermostatController(self.hass, self.entity)
|
||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.entity)
|
||||
|
||||
@@ -977,31 +949,6 @@ class VacuumCapabilities(AlexaEntity):
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(valve.DOMAIN)
|
||||
class ValveCapabilities(AlexaEntity):
|
||||
"""Class to represent Valve capabilities."""
|
||||
|
||||
def default_display_categories(self) -> list[str]:
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.OTHER]
|
||||
|
||||
def interfaces(self) -> Generator[AlexaCapability, None, None]:
|
||||
"""Yield the supported interfaces."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & valve.ValveEntityFeature.SET_POSITION:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{valve.DOMAIN}.{valve.ATTR_POSITION}"
|
||||
)
|
||||
elif supported & (
|
||||
valve.ValveEntityFeature.CLOSE | valve.ValveEntityFeature.OPEN
|
||||
):
|
||||
yield AlexaModeController(self.entity, instance=f"{valve.DOMAIN}.state")
|
||||
if supported & valve.ValveEntityFeature.STOP:
|
||||
yield AlexaToggleController(self.entity, instance=f"{valve.DOMAIN}.stop")
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(camera.DOMAIN)
|
||||
class CameraCapabilities(AlexaEntity):
|
||||
"""Class to represent Camera capabilities."""
|
||||
|
||||
@@ -22,8 +22,6 @@ from homeassistant.components import (
|
||||
number,
|
||||
timer,
|
||||
vacuum,
|
||||
valve,
|
||||
water_heater,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -82,23 +80,6 @@ from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive"
|
||||
|
||||
MIN_MAX_TEMP = {
|
||||
climate.DOMAIN: {
|
||||
"min_temp": climate.ATTR_MIN_TEMP,
|
||||
"max_temp": climate.ATTR_MAX_TEMP,
|
||||
},
|
||||
water_heater.DOMAIN: {
|
||||
"min_temp": water_heater.ATTR_MIN_TEMP,
|
||||
"max_temp": water_heater.ATTR_MAX_TEMP,
|
||||
},
|
||||
}
|
||||
|
||||
SERVICE_SET_TEMPERATURE = {
|
||||
climate.DOMAIN: climate.SERVICE_SET_TEMPERATURE,
|
||||
water_heater.DOMAIN: water_heater.SERVICE_SET_TEMPERATURE,
|
||||
}
|
||||
|
||||
HANDLERS: Registry[
|
||||
tuple[str, str],
|
||||
Callable[
|
||||
@@ -823,10 +804,8 @@ async def async_api_set_target_temp(
|
||||
) -> AlexaResponse:
|
||||
"""Process a set target temperature request."""
|
||||
entity = directive.entity
|
||||
domain = entity.domain
|
||||
|
||||
min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
|
||||
max_temp = entity.attributes["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: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
@@ -870,11 +849,9 @@ async def async_api_set_target_temp(
|
||||
}
|
||||
)
|
||||
|
||||
service = SERVICE_SET_TEMPERATURE[domain]
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain,
|
||||
service,
|
||||
climate.SERVICE_SET_TEMPERATURE,
|
||||
data,
|
||||
blocking=False,
|
||||
context=context,
|
||||
@@ -890,12 +867,11 @@ async def async_api_adjust_target_temp(
|
||||
directive: AlexaDirective,
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process an adjust target temperature request for climates and water heaters."""
|
||||
"""Process an adjust target temperature request."""
|
||||
data: dict[str, Any]
|
||||
entity = directive.entity
|
||||
domain = entity.domain
|
||||
min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
|
||||
max_temp = entity.attributes[MIN_MAX_TEMP[domain]["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(
|
||||
@@ -956,11 +932,9 @@ async def async_api_adjust_target_temp(
|
||||
}
|
||||
)
|
||||
|
||||
service = SERVICE_SET_TEMPERATURE[domain]
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain,
|
||||
service,
|
||||
climate.SERVICE_SET_TEMPERATURE,
|
||||
data,
|
||||
blocking=False,
|
||||
context=context,
|
||||
@@ -1189,23 +1163,6 @@ async def async_api_set_mode(
|
||||
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
# Water heater operation mode
|
||||
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||
operation_mode = mode.split(".")[1]
|
||||
operation_modes: list[str] | None = entity.attributes.get(
|
||||
water_heater.ATTR_OPERATION_LIST
|
||||
)
|
||||
if (
|
||||
operation_mode != PRESET_MODE_NA
|
||||
and operation_modes
|
||||
and operation_mode in operation_modes
|
||||
):
|
||||
service = water_heater.SERVICE_SET_OPERATION_MODE
|
||||
data[water_heater.ATTR_OPERATION_MODE] = operation_mode
|
||||
else:
|
||||
msg = f"Entity '{entity.entity_id}' does not support Operation mode '{operation_mode}'"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
# Cover Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
position = mode.split(".")[1]
|
||||
@@ -1217,15 +1174,6 @@ async def async_api_set_mode(
|
||||
elif position == "custom":
|
||||
service = cover.SERVICE_STOP_COVER
|
||||
|
||||
# Valve position state
|
||||
elif instance == f"{valve.DOMAIN}.state":
|
||||
position = mode.split(".")[1]
|
||||
|
||||
if position == valve.STATE_CLOSED:
|
||||
service = valve.SERVICE_CLOSE_VALVE
|
||||
elif position == valve.STATE_OPEN:
|
||||
service = valve.SERVICE_OPEN_VALVE
|
||||
|
||||
if not service:
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
@@ -1276,23 +1224,16 @@ async def async_api_toggle_on(
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
|
||||
data: dict[str, Any]
|
||||
|
||||
# Fan Oscillating
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
fan.ATTR_OSCILLATING: True,
|
||||
}
|
||||
elif instance == f"{valve.DOMAIN}.stop":
|
||||
service = valve.SERVICE_STOP_VALVE
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
}
|
||||
else:
|
||||
if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
fan.ATTR_OSCILLATING: True,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
@@ -1363,14 +1304,13 @@ async def async_api_set_range(
|
||||
service = None
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_value = directive.payload["rangeValue"]
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
# Cover Position
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_value = int(range_value)
|
||||
if supported & cover.CoverEntityFeature.CLOSE and range_value == 0:
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif supported & cover.CoverEntityFeature.OPEN and range_value == 100:
|
||||
elif range_value == 100:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
else:
|
||||
service = cover.SERVICE_SET_COVER_POSITION
|
||||
@@ -1379,9 +1319,9 @@ async def async_api_set_range(
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_value = int(range_value)
|
||||
if supported & cover.CoverEntityFeature.CLOSE_TILT and range_value == 0:
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||
elif supported & cover.CoverEntityFeature.OPEN_TILT and range_value == 100:
|
||||
elif range_value == 100:
|
||||
service = cover.SERVICE_OPEN_COVER_TILT
|
||||
else:
|
||||
service = cover.SERVICE_SET_COVER_TILT_POSITION
|
||||
@@ -1392,11 +1332,13 @@ async def async_api_set_range(
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
elif supported & fan.FanEntityFeature.SET_SPEED:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
data[fan.ATTR_PERCENTAGE] = range_value
|
||||
else:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported and fan.FanEntityFeature.SET_SPEED:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
data[fan.ATTR_PERCENTAGE] = range_value
|
||||
else:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
|
||||
# Humidifier target humidity
|
||||
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
|
||||
@@ -1434,17 +1376,6 @@ async def async_api_set_range(
|
||||
|
||||
data[vacuum.ATTR_FAN_SPEED] = speed
|
||||
|
||||
# Valve Position
|
||||
elif instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||
range_value = int(range_value)
|
||||
if supported & valve.ValveEntityFeature.CLOSE and range_value == 0:
|
||||
service = valve.SERVICE_CLOSE_VALVE
|
||||
elif supported & valve.ValveEntityFeature.OPEN and range_value == 100:
|
||||
service = valve.SERVICE_OPEN_VALVE
|
||||
else:
|
||||
service = valve.SERVICE_SET_VALVE_POSITION
|
||||
data[valve.ATTR_POSITION] = range_value
|
||||
|
||||
else:
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
@@ -1590,21 +1521,6 @@ async def async_api_adjust_range(
|
||||
)
|
||||
data[vacuum.ATTR_FAN_SPEED] = response_value = speed
|
||||
|
||||
# Valve Position
|
||||
elif instance == f"{valve.DOMAIN}.{valve.ATTR_POSITION}":
|
||||
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
|
||||
service = valve.SERVICE_SET_VALVE_POSITION
|
||||
if not (current := entity.attributes.get(valve.ATTR_POSITION)):
|
||||
msg = f"Unable to determine {entity.entity_id} current position"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
position = response_value = min(100, max(0, range_delta + current))
|
||||
if position == 100:
|
||||
service = valve.SERVICE_OPEN_VALVE
|
||||
elif position == 0:
|
||||
service = valve.SERVICE_CLOSE_VALVE
|
||||
else:
|
||||
data[valve.ATTR_POSITION] = position
|
||||
|
||||
else:
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "amberelectric"
|
||||
CONF_API_TOKEN = "api_token"
|
||||
CONF_SITE_NAME = "site_name"
|
||||
CONF_SITE_ID = "site_id"
|
||||
CONF_SITE_NMI = "site_nmi"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Config flow for Ambiclimate."""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
import ambiclimate
|
||||
@@ -8,8 +7,7 @@ import ambiclimate
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -28,9 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def register_flow_implementation(
|
||||
hass: HomeAssistant, client_id: str, client_secret: str
|
||||
) -> None:
|
||||
def register_flow_implementation(hass, client_id, client_secret):
|
||||
"""Register a ambiclimate implementation.
|
||||
|
||||
client_id: Client id.
|
||||
@@ -54,9 +50,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._registered_view = False
|
||||
self._oauth = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle external yaml configuration."""
|
||||
self._async_abort_entries_match()
|
||||
|
||||
@@ -68,9 +62,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_auth()
|
||||
|
||||
async def async_step_auth(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
async def async_step_auth(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
self._async_abort_entries_match()
|
||||
|
||||
@@ -91,7 +83,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_code(self, code: str | None = None) -> FlowResult:
|
||||
async def async_step_code(self, code=None):
|
||||
"""Received code for authentication."""
|
||||
self._async_abort_entries_match()
|
||||
|
||||
@@ -103,7 +95,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_create_entry(title="Ambiclimate", data=config)
|
||||
|
||||
async def _get_token_info(self, code: str | None) -> dict[str, Any] | None:
|
||||
async def _get_token_info(self, code):
|
||||
oauth = self._generate_oauth()
|
||||
try:
|
||||
token_info = await oauth.get_access_token(code)
|
||||
@@ -111,16 +103,16 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Failed to get access token")
|
||||
return None
|
||||
|
||||
store = Store[dict[str, Any]](self.hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
await store.async_save(token_info)
|
||||
|
||||
return token_info
|
||||
|
||||
def _generate_view(self) -> None:
|
||||
def _generate_view(self):
|
||||
self.hass.http.register_view(AmbiclimateAuthCallbackView())
|
||||
self._registered_view = True
|
||||
|
||||
def _generate_oauth(self) -> ambiclimate.AmbiclimateOAuth:
|
||||
def _generate_oauth(self):
|
||||
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
|
||||
clientsession = async_get_clientsession(self.hass)
|
||||
callback_url = self._cb_url()
|
||||
|
||||
@@ -63,14 +63,14 @@ TYPE_RELAY8 = "relay8"
|
||||
TYPE_RELAY9 = "relay9"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AmbientBinarySensorDescriptionMixin:
|
||||
"""Define an entity description mixin for binary sensors."""
|
||||
|
||||
on_state: Literal[0, 1]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AmbientBinarySensorDescription(
|
||||
BinarySensorEntityDescription, AmbientBinarySensorDescriptionMixin
|
||||
):
|
||||
|
||||
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
||||
from . import AmcrestDevice
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AmcrestSensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describe Amcrest sensor entity."""
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator
|
||||
from .entity import AndroidIPCamBaseEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AndroidIPWebcamSensorEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[PyDroidIPCam], StateType]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AndroidIPWebcamSensorEntityDescription(
|
||||
SensorEntityDescription, AndroidIPWebcamSensorEntityDescriptionMixin
|
||||
):
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of the device running the Android IP Webcam app. The IP address is shown in the app once you start the server."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator
|
||||
from .entity import AndroidIPCamBaseEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AndroidIPWebcamSwitchEntityDescriptionMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
@@ -26,7 +26,7 @@ class AndroidIPWebcamSwitchEntityDescriptionMixin:
|
||||
off_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AndroidIPWebcamSwitchEntityDescription(
|
||||
SwitchEntityDescription, AndroidIPWebcamSwitchEntityDescriptionMixin
|
||||
):
|
||||
|
||||
@@ -160,7 +160,7 @@ def adb_decorator(
|
||||
"""
|
||||
|
||||
def _adb_decorator(
|
||||
func: _FuncType[_ADBDeviceT, _P, _R],
|
||||
func: _FuncType[_ADBDeviceT, _P, _R]
|
||||
) -> _ReturnFuncType[_ADBDeviceT, _P, _R]:
|
||||
"""Wrap the provided ADB method and catch exceptions."""
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from androidtvremote2 import (
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@callback
|
||||
def on_hass_stop(event: Event) -> None:
|
||||
def on_hass_stop(event) -> None:
|
||||
"""Stop push updates when hass stops."""
|
||||
api.disconnect()
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ from .entity import AnovaDescriptionEntity
|
||||
from .models import AnovaData
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AnovaSensorEntityDescriptionMixin:
|
||||
"""Describes the mixin variables for anova sensors."""
|
||||
|
||||
value_fn: Callable[[APCUpdateSensor], float | int | str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class AnovaSensorEntityDescription(
|
||||
SensorEntityDescription, AnovaSensorEntityDescriptionMixin
|
||||
):
|
||||
|
||||
@@ -10,12 +10,18 @@ from anthemav.device_error import DeviceError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_PORT
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DEFAULT_NAME, DEFAULT_PORT, DEVICE_TIMEOUT_SECONDS, DOMAIN
|
||||
from .const import (
|
||||
CONF_MODEL,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PORT,
|
||||
DEVICE_TIMEOUT_SECONDS,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Constants for the Anthem A/V Receivers integration."""
|
||||
ANTHEMAV_UPDATE_SIGNAL = "anthemav_update"
|
||||
|
||||
CONF_MODEL = "model"
|
||||
DEFAULT_NAME = "Anthem AV"
|
||||
DEFAULT_PORT = 14999
|
||||
DOMAIN = "anthemav"
|
||||
|
||||
@@ -13,13 +13,13 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MAC, CONF_MODEL
|
||||
from homeassistant.const import CONF_MAC
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ANTHEMAV_UPDATE_SIGNAL, DOMAIN, MANUFACTURER
|
||||
from .const import ANTHEMAV_UPDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
"""The A. O. Smith integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from py_aosmith import AOSmithAPIClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AOSmithData:
|
||||
"""Data for the A. O. Smith integration."""
|
||||
|
||||
client: AOSmithAPIClient
|
||||
status_coordinator: AOSmithStatusCoordinator
|
||||
energy_coordinator: AOSmithEnergyCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up A. O. Smith from a config entry."""
|
||||
email = entry.data[CONF_EMAIL]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
client = AOSmithAPIClient(email, password, session)
|
||||
|
||||
status_coordinator = AOSmithStatusCoordinator(hass, client)
|
||||
await status_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
for junction_id, status_data in status_coordinator.data.items():
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, junction_id)},
|
||||
manufacturer="A. O. Smith",
|
||||
name=status_data.get("name"),
|
||||
model=status_data.get("model"),
|
||||
serial_number=status_data.get("serial"),
|
||||
suggested_area=status_data.get("install", {}).get("location"),
|
||||
sw_version=status_data.get("data", {}).get("firmwareVersion"),
|
||||
)
|
||||
|
||||
energy_coordinator = AOSmithEnergyCoordinator(
|
||||
hass, client, list(status_coordinator.data)
|
||||
)
|
||||
await energy_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AOSmithData(
|
||||
client,
|
||||
status_coordinator,
|
||||
energy_coordinator,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
@@ -1,107 +0,0 @@
|
||||
"""Config flow for A. O. Smith integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from py_aosmith import AOSmithAPIClient, AOSmithInvalidCredentialsException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for A. O. Smith."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_reauth_email: str | None = None
|
||||
|
||||
async def _async_validate_credentials(
|
||||
self, email: str, password: str
|
||||
) -> str | None:
|
||||
"""Validate the credentials. Return an error string, or None if successful."""
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
client = AOSmithAPIClient(email, password, session)
|
||||
|
||||
try:
|
||||
await client.get_devices()
|
||||
except AOSmithInvalidCredentialsException:
|
||||
return "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return "unknown"
|
||||
|
||||
return None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
unique_id = user_input[CONF_EMAIL].lower()
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
error = await self._async_validate_credentials(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
if error is None:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL], data=user_input
|
||||
)
|
||||
|
||||
errors["base"] = error
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Perform reauth if the user credentials have changed."""
|
||||
self._reauth_email = entry_data[CONF_EMAIL]
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle user's reauth credentials."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None and self._reauth_email is not None:
|
||||
email = self._reauth_email
|
||||
password = user_input[CONF_PASSWORD]
|
||||
entry_id = self.context["entry_id"]
|
||||
|
||||
if entry := self.hass.config_entries.async_get_entry(entry_id):
|
||||
error = await self._async_validate_credentials(email, password)
|
||||
if error is None:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data=entry.data | user_input,
|
||||
)
|
||||
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
errors["base"] = error
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||
description_placeholders={CONF_EMAIL: self._reauth_email},
|
||||
errors=errors,
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Constants for the A. O. Smith integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "aosmith"
|
||||
|
||||
AOSMITH_MODE_ELECTRIC = "ELECTRIC"
|
||||
AOSMITH_MODE_HEAT_PUMP = "HEAT_PUMP"
|
||||
AOSMITH_MODE_HYBRID = "HYBRID"
|
||||
AOSMITH_MODE_VACATION = "VACATION"
|
||||
|
||||
# Update interval to be used for normal background updates.
|
||||
REGULAR_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
# Update interval to be used while a mode or setpoint change is in progress.
|
||||
FAST_INTERVAL = timedelta(seconds=1)
|
||||
|
||||
# Update interval to be used for energy usage data.
|
||||
ENERGY_USAGE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
HOT_WATER_STATUS_MAP = {
|
||||
"LOW": "low",
|
||||
"MEDIUM": "medium",
|
||||
"HIGH": "high",
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
"""The data update coordinator for the A. O. Smith integration."""
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from py_aosmith import (
|
||||
AOSmithAPIClient,
|
||||
AOSmithInvalidCredentialsException,
|
||||
AOSmithUnknownException,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, ENERGY_USAGE_INTERVAL, FAST_INTERVAL, REGULAR_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AOSmithStatusCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
"""Coordinator for device status, updating with a frequent interval."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: AOSmithAPIClient) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=REGULAR_INTERVAL)
|
||||
self.client = client
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Fetch latest data from the device status endpoint."""
|
||||
try:
|
||||
devices = await self.client.get_devices()
|
||||
except AOSmithInvalidCredentialsException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except AOSmithUnknownException as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
mode_pending = any(
|
||||
device.get("data", {}).get("modePending") for device in devices
|
||||
)
|
||||
setpoint_pending = any(
|
||||
device.get("data", {}).get("temperatureSetpointPending")
|
||||
for device in devices
|
||||
)
|
||||
|
||||
if mode_pending or setpoint_pending:
|
||||
self.update_interval = FAST_INTERVAL
|
||||
else:
|
||||
self.update_interval = REGULAR_INTERVAL
|
||||
|
||||
return {device.get("junctionId"): device for device in devices}
|
||||
|
||||
|
||||
class AOSmithEnergyCoordinator(DataUpdateCoordinator[dict[str, float]]):
|
||||
"""Coordinator for energy usage data, updating with a slower interval."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: AOSmithAPIClient,
|
||||
junction_ids: list[str],
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=ENERGY_USAGE_INTERVAL
|
||||
)
|
||||
self.client = client
|
||||
self.junction_ids = junction_ids
|
||||
|
||||
async def _async_update_data(self) -> dict[str, float]:
|
||||
"""Fetch latest data from the energy usage endpoint."""
|
||||
energy_usage_by_junction_id: dict[str, float] = {}
|
||||
|
||||
for junction_id in self.junction_ids:
|
||||
try:
|
||||
energy_usage = await self.client.get_energy_use_data(junction_id)
|
||||
except AOSmithInvalidCredentialsException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except AOSmithUnknownException as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
energy_usage_by_junction_id[junction_id] = energy_usage.get("lifetimeKwh")
|
||||
|
||||
return energy_usage_by_junction_id
|
||||
@@ -1,62 +0,0 @@
|
||||
"""The base entity for the A. O. Smith integration."""
|
||||
from typing import TypeVar
|
||||
|
||||
from py_aosmith import AOSmithAPIClient
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||
|
||||
_AOSmithCoordinatorT = TypeVar(
|
||||
"_AOSmithCoordinatorT", bound=AOSmithStatusCoordinator | AOSmithEnergyCoordinator
|
||||
)
|
||||
|
||||
|
||||
class AOSmithEntity(CoordinatorEntity[_AOSmithCoordinatorT]):
|
||||
"""Base entity for A. O. Smith."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: _AOSmithCoordinatorT, junction_id: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self.junction_id = junction_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, junction_id)},
|
||||
)
|
||||
|
||||
@property
|
||||
def client(self) -> AOSmithAPIClient:
|
||||
"""Shortcut to get the API client."""
|
||||
return self.coordinator.client
|
||||
|
||||
|
||||
class AOSmithStatusEntity(AOSmithEntity[AOSmithStatusCoordinator]):
|
||||
"""Base entity for entities that use data from the status coordinator."""
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Shortcut to get the device status from the coordinator data."""
|
||||
return self.coordinator.data.get(self.junction_id)
|
||||
|
||||
@property
|
||||
def device_data(self):
|
||||
"""Shortcut to get the device data within the device status."""
|
||||
device = self.device
|
||||
return None if device is None else device.get("data", {})
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self.device_data.get("isOnline") is True
|
||||
|
||||
|
||||
class AOSmithEnergyEntity(AOSmithEntity[AOSmithEnergyCoordinator]):
|
||||
"""Base entity for entities that use data from the energy coordinator."""
|
||||
|
||||
@property
|
||||
def energy_usage(self) -> float | None:
|
||||
"""Shortcut to get the energy usage from the coordinator data."""
|
||||
return self.coordinator.data.get(self.junction_id)
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"domain": "aosmith",
|
||||
"name": "A. O. Smith",
|
||||
"codeowners": ["@bdr99"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["py-aosmith==1.0.1"]
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
"""The sensor platform for the A. O. Smith integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AOSmithData
|
||||
from .const import DOMAIN, HOT_WATER_STATUS_MAP
|
||||
from .coordinator import AOSmithEnergyCoordinator, AOSmithStatusCoordinator
|
||||
from .entity import AOSmithEnergyEntity, AOSmithStatusEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AOSmithStatusSensorEntityDescription(SensorEntityDescription):
|
||||
"""Entity description class for sensors using data from the status coordinator."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], str | int | None]
|
||||
|
||||
|
||||
STATUS_ENTITY_DESCRIPTIONS: tuple[AOSmithStatusSensorEntityDescription, ...] = (
|
||||
AOSmithStatusSensorEntityDescription(
|
||||
key="hot_water_availability",
|
||||
translation_key="hot_water_availability",
|
||||
icon="mdi:water-thermometer",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["low", "medium", "high"],
|
||||
value_fn=lambda device: HOT_WATER_STATUS_MAP.get(
|
||||
device.get("data", {}).get("hotWaterStatus")
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up A. O. Smith sensor platform."""
|
||||
data: AOSmithData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
AOSmithStatusSensorEntity(data.status_coordinator, description, junction_id)
|
||||
for description in STATUS_ENTITY_DESCRIPTIONS
|
||||
for junction_id in data.status_coordinator.data
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
AOSmithEnergySensorEntity(data.energy_coordinator, junction_id)
|
||||
for junction_id in data.status_coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AOSmithStatusSensorEntity(AOSmithStatusEntity, SensorEntity):
|
||||
"""Class for sensor entities that use data from the status coordinator."""
|
||||
|
||||
entity_description: AOSmithStatusSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AOSmithStatusCoordinator,
|
||||
description: AOSmithStatusSensorEntityDescription,
|
||||
junction_id: str,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, junction_id)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{description.key}_{junction_id}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.device)
|
||||
|
||||
|
||||
class AOSmithEnergySensorEntity(AOSmithEnergyEntity, SensorEntity):
|
||||
"""Class for the energy sensor entity."""
|
||||
|
||||
_attr_translation_key = "energy_usage"
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_suggested_display_precision = 1
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AOSmithEnergyCoordinator,
|
||||
junction_id: str,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, junction_id)
|
||||
self._attr_unique_id = f"energy_usage_{junction_id}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.energy_usage
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"description": "Please enter your A. O. Smith credentials."
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Please update your password for {email}",
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"hot_water_availability": {
|
||||
"name": "Hot water availability",
|
||||
"state": {
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High"
|
||||
}
|
||||
},
|
||||
"energy_usage": {
|
||||
"name": "Energy usage"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
"""The water heater platform for the A. O. Smith integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
STATE_ELECTRIC,
|
||||
STATE_HEAT_PUMP,
|
||||
STATE_OFF,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AOSmithData
|
||||
from .const import (
|
||||
AOSMITH_MODE_ELECTRIC,
|
||||
AOSMITH_MODE_HEAT_PUMP,
|
||||
AOSMITH_MODE_HYBRID,
|
||||
AOSMITH_MODE_VACATION,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import AOSmithStatusCoordinator
|
||||
from .entity import AOSmithStatusEntity
|
||||
|
||||
MODE_HA_TO_AOSMITH = {
|
||||
STATE_OFF: AOSMITH_MODE_VACATION,
|
||||
STATE_ECO: AOSMITH_MODE_HYBRID,
|
||||
STATE_ELECTRIC: AOSMITH_MODE_ELECTRIC,
|
||||
STATE_HEAT_PUMP: AOSMITH_MODE_HEAT_PUMP,
|
||||
}
|
||||
MODE_AOSMITH_TO_HA = {
|
||||
AOSMITH_MODE_ELECTRIC: STATE_ELECTRIC,
|
||||
AOSMITH_MODE_HEAT_PUMP: STATE_HEAT_PUMP,
|
||||
AOSMITH_MODE_HYBRID: STATE_ECO,
|
||||
AOSMITH_MODE_VACATION: STATE_OFF,
|
||||
}
|
||||
|
||||
# Operation mode to use when exiting away mode
|
||||
DEFAULT_OPERATION_MODE = AOSMITH_MODE_HYBRID
|
||||
|
||||
DEFAULT_SUPPORT_FLAGS = (
|
||||
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up A. O. Smith water heater platform."""
|
||||
data: AOSmithData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
AOSmithWaterHeaterEntity(data.status_coordinator, junction_id)
|
||||
for junction_id in data.status_coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
|
||||
"""The water heater entity for the A. O. Smith integration."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
_attr_min_temp = 95
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AOSmithStatusCoordinator,
|
||||
junction_id: str,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, junction_id)
|
||||
self._attr_unique_id = junction_id
|
||||
|
||||
@property
|
||||
def operation_list(self) -> list[str]:
|
||||
"""Return the list of supported operation modes."""
|
||||
op_modes = []
|
||||
for mode_dict in self.device_data.get("modes", []):
|
||||
mode_name = mode_dict.get("mode")
|
||||
ha_mode = MODE_AOSMITH_TO_HA.get(mode_name)
|
||||
|
||||
# Filtering out STATE_OFF since it is handled by away mode
|
||||
if ha_mode is not None and ha_mode != STATE_OFF:
|
||||
op_modes.append(ha_mode)
|
||||
|
||||
return op_modes
|
||||
|
||||
@property
|
||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
supports_vacation_mode = any(
|
||||
mode_dict.get("mode") == AOSMITH_MODE_VACATION
|
||||
for mode_dict in self.device_data.get("modes", [])
|
||||
)
|
||||
|
||||
if supports_vacation_mode:
|
||||
return DEFAULT_SUPPORT_FLAGS | WaterHeaterEntityFeature.AWAY_MODE
|
||||
|
||||
return DEFAULT_SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.device_data.get("temperatureSetpoint")
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum temperature."""
|
||||
return self.device_data.get("temperatureSetpointMaximum")
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str:
|
||||
"""Return the current operation mode."""
|
||||
return MODE_AOSMITH_TO_HA.get(self.device_data.get("mode"), STATE_OFF)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return True if away mode is on."""
|
||||
return self.device_data.get("mode") == AOSMITH_MODE_VACATION
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new target operation mode."""
|
||||
aosmith_mode = MODE_HA_TO_AOSMITH.get(operation_mode)
|
||||
if aosmith_mode is not None:
|
||||
await self.client.update_mode(self.junction_id, aosmith_mode)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get("temperature")
|
||||
await self.client.update_setpoint(self.junction_id, temperature)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Turn away mode on."""
|
||||
await self.client.update_mode(self.junction_id, AOSMITH_MODE_VACATION)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_away_mode_off(self) -> None:
|
||||
"""Turn away mode off."""
|
||||
await self.client.update_mode(self.junction_id, DEFAULT_OPERATION_MODE)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -7,9 +7,8 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
import aioapcaccess
|
||||
from apcaccess import status
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -33,8 +32,6 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
||||
updates from the server.
|
||||
"""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, host: str, port: int) -> None:
|
||||
"""Initialize the data object."""
|
||||
super().__init__(
|
||||
@@ -73,10 +70,13 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
||||
return self.data.get("SERIALNO")
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
def device_info(self) -> DeviceInfo | None:
|
||||
"""Return the DeviceInfo of this APC UPS, if serial number is available."""
|
||||
if not self.ups_serial_no:
|
||||
return None
|
||||
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.ups_serial_no or self.config_entry.entry_id)},
|
||||
identifiers={(DOMAIN, self.ups_serial_no)},
|
||||
model=self.ups_model,
|
||||
manufacturer="APC",
|
||||
name=self.ups_name if self.ups_name else "APC UPS",
|
||||
@@ -90,8 +90,13 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
|
||||
Note that the result dict uses upper case for each resource, where our
|
||||
integration uses lower cases as keys internally.
|
||||
"""
|
||||
|
||||
async with asyncio.timeout(10):
|
||||
try:
|
||||
return await aioapcaccess.request_status(self._host, self._port)
|
||||
except (OSError, asyncio.IncompleteReadError) as error:
|
||||
raw = await self.hass.async_add_executor_job(
|
||||
status.get, self._host, self._port
|
||||
)
|
||||
result: OrderedDict[str, str] = status.parse(raw)
|
||||
return result
|
||||
except OSError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["apcaccess"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioapcaccess==0.4.2"]
|
||||
"requirements": ["apcaccess==0.0.13"]
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ from homeassistant.exceptions import (
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.aiohttp_compat import enable_compression
|
||||
from homeassistant.helpers.event import EventStateChangedData
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
@@ -217,11 +218,9 @@ class APIStatesView(HomeAssistantView):
|
||||
if entity_perm(state.entity_id, "read")
|
||||
)
|
||||
response = web.Response(
|
||||
body=f'[{",".join(states)}]',
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
zlib_executor_size=32768,
|
||||
body=f'[{",".join(states)}]', content_type=CONTENT_TYPE_JSON
|
||||
)
|
||||
response.enable_compression()
|
||||
enable_compression(response)
|
||||
return response
|
||||
|
||||
|
||||
@@ -391,14 +390,17 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
)
|
||||
|
||||
try:
|
||||
# shield the service call from cancellation on connection drop
|
||||
await shield(
|
||||
hass.services.async_call(
|
||||
domain, service, data, blocking=True, context=context
|
||||
async with timeout(SERVICE_WAIT_TIMEOUT):
|
||||
# shield the service call from cancellation on connection drop
|
||||
await shield(
|
||||
hass.services.async_call(
|
||||
domain, service, data, blocking=True, context=context
|
||||
)
|
||||
)
|
||||
)
|
||||
except (vol.Invalid, ServiceNotFound) as ex:
|
||||
raise HTTPBadRequest() from ex
|
||||
except TimeoutError:
|
||||
pass
|
||||
finally:
|
||||
cancel_listen()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user