This commit is contained in:
Franck Nijhof 2024-01-03 18:15:49 +01:00 committed by GitHub
commit fb0cc6c5d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2192 changed files with 68473 additions and 17155 deletions

View File

@ -173,6 +173,7 @@ 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
@ -337,7 +338,6 @@ 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,8 +364,6 @@ 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
@ -404,6 +402,9 @@ 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
@ -419,6 +420,7 @@ 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
@ -536,6 +538,7 @@ 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
@ -754,6 +757,9 @@ 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
@ -799,7 +805,8 @@ omit =
homeassistant/components/netgear/sensor.py
homeassistant/components/netgear/switch.py
homeassistant/components/netgear/update.py
homeassistant/components/netgear_lte/*
homeassistant/components/netgear_lte/__init__.py
homeassistant/components/netgear_lte/notify.py
homeassistant/components/netio/switch.py
homeassistant/components/neurio_energy/sensor.py
homeassistant/components/nexia/climate.py
@ -900,6 +907,9 @@ 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
@ -1026,6 +1036,12 @@ 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/*
@ -1209,6 +1225,7 @@ 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
@ -1226,8 +1243,12 @@ omit =
homeassistant/components/stream/fmp4utils.py
homeassistant/components/stream/hls.py
homeassistant/components/stream/worker.py
homeassistant/components/streamlabswater/*
homeassistant/components/suez_water/*
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/supervisord/sensor.py
homeassistant/components/supla/*
homeassistant/components/surepetcare/__init__.py
@ -1235,6 +1256,8 @@ 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
@ -1286,7 +1309,9 @@ 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
@ -1375,10 +1400,6 @@ 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
@ -1413,6 +1434,8 @@ 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/*

View File

@ -29,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@ -331,7 +331,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Install Cosign
uses: sigstore/cosign-installer@v3.2.0
uses: sigstore/cosign-installer@v3.3.0
with:
cosign-release: "v2.0.2"

View File

@ -36,7 +36,7 @@ env:
CACHE_VERSION: 5
PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 6
HA_SHORT_VERSION: "2023.12"
HA_SHORT_VERSION: "2024.1"
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
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@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true

View File

@ -29,11 +29,11 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Initialize CodeQL
uses: github/codeql-action/init@v2.22.8
uses: github/codeql-action/init@v3.22.12
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2.22.8
uses: github/codeql-action/analyze@v3.22.12
with:
category: "/language:python"

View File

@ -11,16 +11,16 @@ jobs:
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
steps:
# The 90 day stale policy for PRs
# The 60 day stale policy for PRs
# Used for:
# - PRs
# - No PRs marked as no-stale
# - No issues (-1)
- name: 90 days stale PRs policy
uses: actions/stale@v8.0.0
- name: 60 days stale PRs policy
uses: actions/stale@v9.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
days-before-stale: 60
days-before-close: 7
days-before-issue-stale: -1
days-before-issue-close: -1
@ -33,7 +33,11 @@ jobs:
pull request has been automatically marked as stale because of that
and will be closed if no further activity occurs within 7 days.
Thank you for your contributions.
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!
# 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.
@ -53,7 +57,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@v8.0.0
uses: actions/stale@v9.0.0
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@ -83,7 +87,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@v8.0.0
uses: actions/stale@v9.0.0
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"

View File

@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.1
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
rev: v0.1.8
hooks:
- id: ruff
args:

View File

@ -43,11 +43,14 @@ 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.*
@ -59,10 +62,14 @@ 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.*
@ -75,6 +82,7 @@ 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.*
@ -117,9 +125,12 @@ 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.*
@ -127,6 +138,7 @@ 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.*
@ -150,6 +162,7 @@ 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.*
@ -228,6 +241,7 @@ 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.*
@ -264,6 +278,7 @@ 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.*
@ -313,6 +328,8 @@ 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.*
@ -323,6 +340,7 @@ 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.*
@ -353,6 +371,7 @@ 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.*

View File

@ -86,6 +86,8 @@ 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
@ -153,6 +155,8 @@ 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
@ -193,6 +197,8 @@ 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
@ -205,8 +211,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
/tests/components/co2signal/ @jpbede
/homeassistant/components/co2signal/ @jpbede @VIKTORVAV99
/tests/components/co2signal/ @jpbede @VIKTORVAV99
/homeassistant/components/coinbase/ @tombrien
/tests/components/coinbase/ @tombrien
/homeassistant/components/color_extractor/ @GenericStudent
@ -295,6 +301,8 @@ 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
@ -344,7 +352,7 @@ build.json @home-assistant/supervisor
/tests/components/energy/ @home-assistant/core
/homeassistant/components/energyzero/ @klaasnicolaas
/tests/components/energyzero/ @klaasnicolaas
/homeassistant/components/enigma2/ @fbradyirl
/homeassistant/components/enigma2/ @autinerd
/homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
@ -395,6 +403,8 @@ 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
@ -410,8 +420,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/forked_daapd/ @uvjustin
/tests/components/forked_daapd/ @uvjustin
/homeassistant/components/fortios/ @kimfrellsen
/homeassistant/components/foscam/ @skgsergio
/tests/components/foscam/ @skgsergio
/homeassistant/components/foscam/ @skgsergio @krmarien
/tests/components/foscam/ @skgsergio @krmarien
/homeassistant/components/freebox/ @hacf-fr @Quentame
/tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415
@ -520,6 +530,8 @@ 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
@ -803,6 +815,8 @@ 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
@ -833,6 +847,7 @@ 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
@ -926,6 +941,8 @@ 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
@ -985,8 +1002,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
/homeassistant/components/prusalink/ @balloob
/tests/components/prusalink/ @balloob
/homeassistant/components/prusalink/ @balloob @Skaronator
/tests/components/prusalink/ @balloob @Skaronator
/homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45
/homeassistant/components/pure_energie/ @klaasnicolaas
@ -1003,8 +1020,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
/tests/components/qbittorrent/ @geoffreylagaisse
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
/homeassistant/components/qingping/ @bdraco @skgsergio
/tests/components/qingping/ @bdraco @skgsergio
/homeassistant/components/qld_bushfire/ @exxamalte
@ -1046,6 +1063,8 @@ 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
@ -1058,6 +1077,8 @@ 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
@ -1243,13 +1264,17 @@ 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
/homeassistant/components/swiss_public_transport/ @fabaff @miaucl
/tests/components/swiss_public_transport/ @fabaff @miaucl
/homeassistant/components/switch/ @home-assistant/core
/tests/components/switch/ @home-assistant/core
/homeassistant/components/switch_as_x/ @home-assistant/core
@ -1272,12 +1297,16 @@ build.json @home-assistant/supervisor
/homeassistant/components/synology_srm/ @aerialls
/homeassistant/components/system_bridge/ @timmo001
/tests/components/system_bridge/ @timmo001
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
/tests/components/tado/ @michaelarnauts @chiefdragon
/homeassistant/components/systemmonitor/ @gjohansson-ST
/tests/components/systemmonitor/ @gjohansson-ST
/homeassistant/components/tado/ @michaelarnauts @chiefdragon @erwindouna
/tests/components/tado/ @michaelarnauts @chiefdragon @erwindouna
/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
@ -1293,6 +1322,8 @@ 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
@ -1360,6 +1391,7 @@ 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
@ -1388,6 +1420,8 @@ 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
@ -1396,8 +1430,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/versasense/ @imstevenxyz
/homeassistant/components/version/ @ludeeus
/tests/components/version/ @ludeeus
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
/homeassistant/components/vicare/ @CFenner
/tests/components/vicare/ @CFenner
/homeassistant/components/vilfo/ @ManneW

View File

@ -6,7 +6,7 @@ FROM ${BUILD_FROM}
# Synchronize with homeassistant/core.py:async_stop
ENV \
S6_SERVICES_GRACETIME=220000
S6_SERVICES_GRACETIME=240000
ARG QEMU_CPU

View File

@ -27,6 +27,7 @@ from .const import (
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
config_validation as cv,
device_registry,
entity,
entity_registry,
@ -473,7 +474,9 @@ 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 = {key.partition(" ")[0] for key in config if key != core.DOMAIN}
domains = {
domain for key in config if (domain := cv.domain_key(key)) != core.DOMAIN
}
# Add config entry domains
if not hass.config.recovery_mode:

View File

@ -0,0 +1,5 @@
{
"domain": "flexit",
"name": "Flexit",
"integrations": ["flexit", "flexit_bacnet"]
}

View File

@ -27,7 +27,7 @@ ABODE_TEMPERATURE_UNIT_HA_UNIT = {
}
@dataclass
@dataclass(frozen=True)
class AbodeSensorDescriptionMixin:
"""Mixin for Abode sensor."""
@ -35,7 +35,7 @@ class AbodeSensorDescriptionMixin:
native_unit_of_measurement_fn: Callable[[AbodeSense], str]
@dataclass
@dataclass(frozen=True)
class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin):
"""Class describing Abode sensor entities."""

View File

@ -45,14 +45,14 @@ from .const import (
PARALLEL_UPDATES = 1
@dataclass
@dataclass(frozen=True)
class AccuWeatherSensorDescriptionMixin:
"""Mixin for AccuWeather sensor."""
value_fn: Callable[[dict[str, Any]], str | int | float | None]
@dataclass
@dataclass(frozen=True)
class AccuWeatherSensorDescription(
SensorEntityDescription, AccuWeatherSensorDescriptionMixin
):

View File

@ -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(
hass.config_entries.async_update_entry( # type: ignore[unreachable]
config_entry,
unique_id=str(config_entry.unique_id),
title=str(config_entry.title),

View File

@ -137,7 +137,7 @@ class LocalAdaxDevice(ClimateEntity):
_attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(self, adax_data_handler, unique_id):
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None:
"""Initialize the heater."""
self._adax_data_handler = adax_data_handler
self._attr_unique_id = unique_id

View File

@ -36,7 +36,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 2
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
data_schema = vol.Schema(
{
@ -59,7 +61,9 @@ 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=None):
async def async_step_local(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the local step."""
data_schema = vol.Schema(
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}

View File

@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 4
@dataclass(kw_only=True)
@dataclass(frozen=True, kw_only=True)
class AdGuardHomeEntityDescription(SensorEntityDescription):
"""Describes AdGuard Home sensor entity."""

View File

@ -21,7 +21,7 @@ SCAN_INTERVAL = timedelta(seconds=10)
PARALLEL_UPDATES = 1
@dataclass(kw_only=True)
@dataclass(frozen=True, kw_only=True)
class AdGuardHomeSwitchEntityDescription(SwitchEntityDescription):
"""Describes AdGuard Home switch entity."""

View File

@ -8,6 +8,7 @@ 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
@ -26,6 +27,7 @@ PLATFORMS = [
]
_LOGGER = logging.getLogger(__name__)
REQUEST_REFRESH_DELAY = 0.5
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -51,6 +53,9 @@ 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()

View File

@ -21,6 +21,7 @@ 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,
@ -39,16 +40,6 @@ 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"
@ -56,6 +47,7 @@ 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
@ -85,27 +77,25 @@ async def async_setup_entry(
class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
"""AdvantageAir AC unit."""
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_AUTO]
_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
@ -118,10 +108,6 @@ 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."""
@ -151,7 +137,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
@property
def fan_mode(self) -> str | None:
"""Return the current fan modes."""
return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"])
return FAN_AUTO if self._ac["fan"] == ADVANTAGE_AIR_MYFAN else self._ac["fan"]
@property
def target_temperature_high(self) -> float | None:
@ -189,7 +175,11 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the Fan Mode."""
await self.async_update_ac({"fan": HASS_FAN_MODES.get(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})
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the Temperature."""

View File

@ -5,3 +5,4 @@ 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"

View File

@ -30,7 +30,7 @@ class AdvantageAirEntity(CoordinatorEntity):
async def update_handle(*values):
try:
if await func(*keys, *values):
await self.coordinator.async_refresh()
await self.coordinator.async_request_refresh()
except ApiError as err:
raise HomeAssistantError(err) from err

View File

@ -7,6 +7,7 @@ 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,
@ -29,6 +30,8 @@ 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
@ -62,6 +65,32 @@ 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."""

View File

@ -1,6 +1,8 @@
"""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
@ -8,6 +10,7 @@ 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,
@ -29,7 +32,9 @@ OPTIONS_FLOW = {
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AEMET OpenData."""
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
errors = {}

View File

@ -0,0 +1 @@
"""Virtual integration: AEP Ohio."""

View File

@ -0,0 +1,6 @@
{
"domain": "aep_ohio",
"name": "AEP Ohio",
"integration_type": "virtual",
"supported_by": "opower"
}

View File

@ -0,0 +1 @@
"""Virtual integration: AEP Texas."""

View File

@ -0,0 +1,6 @@
{
"domain": "aep_texas",
"name": "AEP Texas",
"integration_type": "virtual",
"supported_by": "opower"
}

View File

@ -1,5 +1,6 @@
"""Config flow to configure Agent devices."""
from contextlib import suppress
from typing import Any
from agent import AgentConnectionError, AgentError
from agent.a import Agent
@ -7,6 +8,7 @@ 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
@ -18,11 +20,9 @@ DEFAULT_PORT = 8090
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an Agent config flow."""
def __init__(self):
"""Initialize the Agent config flow."""
self.device_config = {}
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle an Agent config flow."""
errors = {}
@ -49,13 +49,15 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
)
self.device_config = {
device_config = {
CONF_HOST: host,
CONF_PORT: port,
SERVER_URL: server_origin,
}
return await self._create_entry(agent_client.name)
return self.async_create_entry(
title=agent_client.name, data=device_config
)
errors["base"] = "cannot_connect"
@ -66,11 +68,6 @@ 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)

View File

@ -56,7 +56,7 @@ from .const import (
PARALLEL_UPDATES = 1
@dataclass
@dataclass(frozen=True)
class AirlySensorEntityDescription(SensorEntityDescription):
"""Class describing Airly sensor entities."""

View File

@ -6,8 +6,17 @@ from pyairnow import WebServiceAPI
from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
import voluptuous as vol
from homeassistant import config_entries, core, data_entry_flow, exceptions
from homeassistant import core
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
OptionsFlow,
OptionsFlowWithConfigEntry,
)
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
@ -16,7 +25,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def validate_input(hass: core.HomeAssistant, data):
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
@ -46,12 +55,14 @@ async def validate_input(hass: core.HomeAssistant, data):
return True
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AirNow."""
VERSION = 2
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
@ -108,18 +119,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod
@core.callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Return the options flow."""
return OptionsFlowHandler(config_entry)
return AirNowOptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle an options flow for AirNow."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> data_entry_flow.FlowResult:
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(data=user_input)
@ -141,13 +152,13 @@ class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
)
class CannotConnect(exceptions.HomeAssistantError):
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class InvalidLocation(exceptions.HomeAssistantError):
class InvalidLocation(HomeAssistantError):
"""Error to indicate the location is invalid."""

View File

@ -1,11 +1,15 @@
"""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 (
@ -31,12 +35,19 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""The AirNow update coordinator."""
def __init__(
self, hass, session, api_key, latitude, longitude, distance, update_interval
):
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
latitude: float,
longitude: float,
distance: int,
update_interval: timedelta,
) -> None:
"""Initialize."""
self.latitude = latitude
self.longitude = longitude
@ -46,7 +57,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self):
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
data = {}
try:

View File

@ -51,7 +51,7 @@ ATTR_LEVEL = "level"
ATTR_STATION = "reporting_station"
@dataclass
@dataclass(frozen=True)
class AirNowEntityDescriptionMixin:
"""Mixin for required keys."""
@ -59,7 +59,7 @@ class AirNowEntityDescriptionMixin:
extra_state_attributes_fn: Callable[[Any], dict[str, str]] | None
@dataclass
@dataclass(frozen=True)
class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMixin):
"""Describes Airnow sensor entity."""

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from typing import Any
from aioairq import AirQ, InvalidAuth, InvalidInput
from aioairq import AirQ, InvalidAuth
from aiohttp.client_exceptions import ClientConnectionError
import voluptuous as vol
@ -42,44 +42,32 @@ 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:
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
except InvalidInput:
await airq.validate()
except ClientConnectionError:
_LOGGER.debug(
"%s does not appear to be a valid IP address or mDNS name",
(
"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"] = "invalid_input"
errors["base"] = "cannot_connect"
except InvalidAuth:
_LOGGER.debug(
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
)
errors["base"] = "invalid_auth"
else:
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]
)
_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

View File

@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.3.1"]
"requirements": ["aioairq==0.3.2"]
}

View File

@ -37,14 +37,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
@dataclass
@dataclass(frozen=True)
class AirQEntityDescriptionMixin:
"""Class for keys required by AirQ entity."""
value: Callable[[dict], float | int | None]
@dataclass
@dataclass(frozen=True)
class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin):
"""Describes AirQ sensor entity."""

View File

@ -7,12 +7,12 @@ import logging
from airthings import Airthings, AirthingsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_ID, 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_ID, CONF_SECRET, DOMAIN
from .const import CONF_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)

View File

@ -8,10 +8,11 @@ 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_ID, CONF_SECRET, DOMAIN
from .const import CONF_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)

View File

@ -2,5 +2,4 @@
DOMAIN = "airthings"
CONF_ID = "id"
CONF_SECRET = "secret"

View File

@ -1,6 +1,7 @@
"""Support for airthings ble sensors."""
from __future__ import annotations
import dataclasses
import logging
from airthings_ble import AirthingsDevice
@ -167,10 +168,13 @@ async def async_setup_entry(
# we need to change some units
sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy()
if not is_metric:
for val in sensors_mapping.values():
for key, val in sensors_mapping.items():
if val.native_unit_of_measurement is not VOLUME_BECQUEREL:
continue
val.native_unit_of_measurement = VOLUME_PICOCURIE
sensors_mapping[key] = dataclasses.replace(
val,
native_unit_of_measurement=VOLUME_PICOCURIE,
)
entities = []
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)

View File

@ -19,6 +19,7 @@ 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,
@ -44,7 +45,6 @@ from homeassistant.helpers.update_coordinator import (
from .const import (
CONF_CITY,
CONF_COUNTRY,
CONF_GEOGRAPHIES,
CONF_INTEGRATION_TYPE,
DOMAIN,

View File

@ -19,6 +19,7 @@ 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,
@ -35,7 +36,6 @@ 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,

View File

@ -9,6 +9,5 @@ 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"

View File

@ -7,6 +7,7 @@ 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,
@ -15,7 +16,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_CITY, CONF_COUNTRY, DOMAIN
from .const import CONF_CITY, DOMAIN
CONF_COORDINATES = "coordinates"
CONF_TITLE = "title"

View File

@ -15,6 +15,7 @@ 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,
@ -25,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import AirVisualEntity
from .const import CONF_CITY, CONF_COUNTRY, DOMAIN
from .const import CONF_CITY, DOMAIN
ATTR_CITY = "city"
ATTR_COUNTRY = "country"

View File

@ -26,7 +26,7 @@ from . import AirVisualProData, AirVisualProEntity
from .const import DOMAIN
@dataclass
@dataclass(frozen=True)
class AirVisualProMeasurementKeyMixin:
"""Define an entity description mixin to include a measurement key."""
@ -35,7 +35,7 @@ class AirVisualProMeasurementKeyMixin:
]
@dataclass
@dataclass(frozen=True)
class AirVisualProMeasurementDescription(
SensorEntityDescription, AirVisualProMeasurementKeyMixin
):

View File

@ -29,7 +29,7 @@ from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
@dataclass
@dataclass(frozen=True)
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
"""A class that describes airzone binary sensor entities."""

View File

@ -26,7 +26,7 @@ from .coordinator import AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneZoneEntity
@dataclass
@dataclass(frozen=True)
class AirzoneSelectDescriptionMixin:
"""Define an entity description mixin for select entities."""
@ -34,7 +34,7 @@ class AirzoneSelectDescriptionMixin:
options_dict: dict[str, int]
@dataclass
@dataclass(frozen=True)
class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescriptionMixin):
"""Class to describe an Airzone select entity."""

View File

@ -34,7 +34,7 @@ from .entity import (
)
@dataclass
@dataclass(frozen=True)
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
"""A class that describes Airzone Cloud binary sensor entities."""

View File

@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -33,6 +34,34 @@ 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):

View File

@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"iot_class": "cloud_polling",
"loggers": ["aladdin_connect"],
"quality_scale": "platinum",
"requirements": ["AIOAladdinConnect==0.1.58"]
}

View File

@ -23,14 +23,14 @@ from .const import DOMAIN
from .model import DoorDevice
@dataclass
@dataclass(frozen=True)
class AccSensorEntityDescriptionMixin:
"""Mixin for required keys."""
value_fn: Callable
@dataclass
@dataclass(frozen=True)
class AccSensorEntityDescription(
SensorEntityDescription, AccSensorEntityDescriptionMixin
):

View File

@ -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 Any, Final, final
from typing import TYPE_CHECKING, Any, Final, final
import voluptuous as vol
@ -23,26 +23,41 @@ 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)
@ -121,12 +136,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await component.async_unload_entry(entry)
@dataclass
class AlarmControlPanelEntityDescription(EntityDescription):
class AlarmControlPanelEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes alarm control panel entities."""
class AlarmControlPanelEntity(Entity):
CACHED_PROPERTIES_WITH_ATTR_ = {
"code_format",
"changed_by",
"code_arm_required",
"supported_features",
}
class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""An abstract class for alarm control entities."""
entity_description: AlarmControlPanelEntityDescription
@ -137,17 +159,17 @@ class AlarmControlPanelEntity(Entity):
AlarmControlPanelEntityFeature(0)
)
@property
@cached_property
def code_format(self) -> CodeFormat | None:
"""Code format or None if no code is required."""
return self._attr_code_format
@property
@cached_property
def changed_by(self) -> str | None:
"""Last change triggered by."""
return self._attr_changed_by
@property
@cached_property
def code_arm_required(self) -> bool:
"""Whether the code is required for arm actions."""
return self._attr_code_arm_required
@ -208,10 +230,15 @@ class AlarmControlPanelEntity(Entity):
"""Send arm custom bypass command."""
await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
@property
@cached_property
def supported_features(self) -> AlarmControlPanelEntityFeature:
"""Return the list of supported features."""
return self._attr_supported_features
features = self._attr_supported_features
if type(features) is int: # noqa: E721
new_features = AlarmControlPanelEntityFeature(features)
self._report_deprecated_supported_features_values(new_features)
return new_features
return features
@final
@property

View File

@ -1,7 +1,14 @@
"""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"
@ -15,10 +22,10 @@ class CodeFormat(StrEnum):
NUMBER = "number"
# These constants are deprecated as of Home Assistant 2022.5
# These constants are deprecated as of Home Assistant 2022.5, can be removed in 2025.1
# Please use the CodeFormat enum instead.
FORMAT_TEXT: Final = "text"
FORMAT_NUMBER: Final = "number"
_DEPRECATED_FORMAT_TEXT: Final = DeprecatedConstantEnum(CodeFormat.TEXT, "2025.1")
_DEPRECATED_FORMAT_NUMBER: Final = DeprecatedConstantEnum(CodeFormat.NUMBER, "2025.1")
class AlarmControlPanelEntityFeature(IntFlag):
@ -34,12 +41,28 @@ class AlarmControlPanelEntityFeature(IntFlag):
# These constants are deprecated as of Home Assistant 2022.5
# Please use the AlarmControlPanelEntityFeature enum instead.
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
_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())
CONDITION_TRIGGERED: Final = "is_triggered"
CONDITION_DISARMED: Final = "is_disarmed"

View File

@ -28,13 +28,7 @@ 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 (
SUPPORT_ALARM_ARM_AWAY,
SUPPORT_ALARM_ARM_HOME,
SUPPORT_ALARM_ARM_NIGHT,
SUPPORT_ALARM_ARM_VACATION,
SUPPORT_ALARM_TRIGGER,
)
from .const import AlarmControlPanelEntityFeature
ACTION_TYPES: Final[set[str]] = {
"arm_away",
@ -82,16 +76,16 @@ async def async_get_actions(
}
# Add actions for each entity that belongs to this integration
if supported_features & SUPPORT_ALARM_ARM_AWAY:
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
actions.append({**base_action, CONF_TYPE: "arm_away"})
if supported_features & SUPPORT_ALARM_ARM_HOME:
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
actions.append({**base_action, CONF_TYPE: "arm_home"})
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
actions.append({**base_action, CONF_TYPE: "arm_night"})
if supported_features & SUPPORT_ALARM_ARM_VACATION:
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
actions.append({**base_action, CONF_TYPE: "arm_vacation"})
actions.append({**base_action, CONF_TYPE: "disarm"})
if supported_features & SUPPORT_ALARM_TRIGGER:
if supported_features & AlarmControlPanelEntityFeature.TRIGGER:
actions.append({**base_action, CONF_TYPE: "trigger"})
return actions

View File

@ -39,11 +39,7 @@ from .const import (
CONDITION_ARMED_VACATION,
CONDITION_DISARMED,
CONDITION_TRIGGERED,
SUPPORT_ALARM_ARM_AWAY,
SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
SUPPORT_ALARM_ARM_HOME,
SUPPORT_ALARM_ARM_NIGHT,
SUPPORT_ALARM_ARM_VACATION,
AlarmControlPanelEntityFeature,
)
CONDITION_TYPES: Final[set[str]] = {
@ -90,15 +86,15 @@ async def async_get_conditions(
{**base_condition, CONF_TYPE: CONDITION_DISARMED},
{**base_condition, CONF_TYPE: CONDITION_TRIGGERED},
]
if supported_features & SUPPORT_ALARM_ARM_HOME:
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_HOME})
if supported_features & SUPPORT_ALARM_ARM_AWAY:
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_AWAY})
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_NIGHT})
if supported_features & SUPPORT_ALARM_ARM_VACATION:
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
conditions.append({**base_condition, CONF_TYPE: CONDITION_ARMED_VACATION})
if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS:
if supported_features & AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS:
conditions.append(
{**base_condition, CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS}
)

View File

@ -29,12 +29,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN
from .const import (
SUPPORT_ALARM_ARM_AWAY,
SUPPORT_ALARM_ARM_HOME,
SUPPORT_ALARM_ARM_NIGHT,
SUPPORT_ALARM_ARM_VACATION,
)
from .const import AlarmControlPanelEntityFeature
BASIC_TRIGGER_TYPES: Final[set[str]] = {"triggered", "disarmed", "arming"}
TRIGGER_TYPES: Final[set[str]] = BASIC_TRIGGER_TYPES | {
@ -82,28 +77,28 @@ async def async_get_triggers(
}
for trigger in BASIC_TRIGGER_TYPES
]
if supported_features & SUPPORT_ALARM_ARM_HOME:
if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
triggers.append(
{
**base_trigger,
CONF_TYPE: "armed_home",
}
)
if supported_features & SUPPORT_ALARM_ARM_AWAY:
if supported_features & AlarmControlPanelEntityFeature.ARM_AWAY:
triggers.append(
{
**base_trigger,
CONF_TYPE: "armed_away",
}
)
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
if supported_features & AlarmControlPanelEntityFeature.ARM_NIGHT:
triggers.append(
{
**base_trigger,
CONF_TYPE: "armed_night",
}
)
if supported_features & SUPPORT_ALARM_ARM_VACATION:
if supported_features & AlarmControlPanelEntityFeature.ARM_VACATION:
triggers.append(
{
**base_trigger,

View File

@ -0,0 +1,41 @@
"""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

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import logging
from typing import Any
from adext import AdExt
from alarmdecoder.devices import SerialDevice, SocketDevice
@ -12,8 +13,10 @@ 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,
@ -66,7 +69,9 @@ 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=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
if user_input is not None:
self.protocol = user_input[CONF_PROTOCOL]
@ -83,7 +88,9 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
),
)
async def async_step_protocol(self, user_input=None):
async def async_step_protocol(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle AlarmDecoder protocol setup."""
errors = {}
if user_input is not None:
@ -146,15 +153,18 @@ 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=None):
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
if user_input[EDIT_KEY] == EDIT_SETTINGS:
@ -173,7 +183,9 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
),
)
async def async_step_arm_settings(self, user_input=None):
async def async_step_arm_settings(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Arming options form."""
if user_input is not None:
return self.async_create_entry(
@ -200,7 +212,9 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
),
)
async def async_step_zone_select(self, user_input=None):
async def async_step_zone_select(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Zone selection form."""
errors = _validate_zone_input(user_input)
@ -216,7 +230,9 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
errors=errors,
)
async def async_step_zone_details(self, user_input=None):
async def async_step_zone_details(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Zone details form."""
errors = _validate_zone_input(user_input)
@ -293,7 +309,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
)
def _validate_zone_input(zone_input):
def _validate_zone_input(zone_input: dict[str, Any] | None) -> dict[str, str]:
if not zone_input:
return {}
errors = {}
@ -327,7 +343,7 @@ def _validate_zone_input(zone_input):
return errors
def _fix_input_types(zone_input):
def _fix_input_types(zone_input: dict[str, Any]) -> dict[str, Any]:
"""Convert necessary keys to int.
Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
@ -341,7 +357,9 @@ def _fix_input_types(zone_input):
return zone_input
def _device_already_added(current_entries, user_input, protocol):
def _device_already_added(
current_entries: list[ConfigEntry], user_input: dict[str, Any], protocol: str | None
) -> bool:
"""Determine if entry has already been added to HA."""
user_host = user_input.get(CONF_HOST)
user_port = user_input.get(CONF_PORT)

View File

@ -36,6 +36,15 @@ 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,
@ -46,7 +55,7 @@ ALEXA_ENTITY_SCHEMA = vol.Schema(
SMART_HOME_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ENDPOINT): cv.string,
vol.Optional(CONF_ENDPOINT): vol.All(vol.Lower, vol.In(VALID_ENDPOINTS)),
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_LOCALE, default=DEFAULT_LOCALE): vol.In(

View File

@ -19,6 +19,8 @@ from homeassistant.components import (
number,
timer,
vacuum,
valve,
water_heater,
)
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
@ -435,7 +437,8 @@ 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
@ -938,6 +941,9 @@ 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
@ -1108,6 +1114,8 @@ 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"})
@ -1127,6 +1135,8 @@ 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
@ -1176,9 +1186,13 @@ 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[climate.ATTR_HVAC_MODES]
hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES, [])
for mode in hvac_modes:
if thermostat_mode := API_THERMOSTAT_MODES.get(mode):
supported_modes.append(thermostat_mode)
@ -1408,6 +1422,16 @@ 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.
@ -1421,6 +1445,19 @@ 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:
@ -1478,6 +1515,26 @@ 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(
@ -1497,6 +1554,32 @@ 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:
@ -1535,6 +1618,34 @@ 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
@ -1648,6 +1759,10 @@ 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:
@ -1771,6 +1886,17 @@ 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:
@ -1847,6 +1973,25 @@ 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
@ -1920,6 +2065,10 @@ 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]]]:
@ -1932,6 +2081,10 @@ 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 {}

View File

@ -32,6 +32,8 @@ from homeassistant.components import (
switch,
timer,
vacuum,
valve,
water_heater,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@ -248,6 +250,9 @@ 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"
@ -456,23 +461,46 @@ 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.
if climate.HVACMode.OFF in self.entity.attributes.get(
climate.ATTR_HVAC_MODES, []
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)
):
yield AlexaPowerController(self.entity)
yield AlexaThermostatController(self.hass, self.entity)
yield AlexaTemperatureSensor(self.hass, 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 AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity)
@ -949,6 +977,31 @@ 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."""

View File

@ -22,6 +22,8 @@ from homeassistant.components import (
number,
timer,
vacuum,
valve,
water_heater,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
@ -80,6 +82,23 @@ 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[
@ -804,8 +823,10 @@ async def async_api_set_target_temp(
) -> AlexaResponse:
"""Process a set target temperature request."""
entity = directive.entity
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
domain = entity.domain
min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
max_temp = entity.attributes["max_temp"]
unit = hass.config.units.temperature_unit
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
@ -849,9 +870,11 @@ async def async_api_set_target_temp(
}
)
service = SERVICE_SET_TEMPERATURE[domain]
await hass.services.async_call(
entity.domain,
climate.SERVICE_SET_TEMPERATURE,
service,
data,
blocking=False,
context=context,
@ -867,11 +890,12 @@ async def async_api_adjust_target_temp(
directive: AlexaDirective,
context: ha.Context,
) -> AlexaResponse:
"""Process an adjust target temperature request."""
"""Process an adjust target temperature request for climates and water heaters."""
data: dict[str, Any]
entity = directive.entity
min_temp = entity.attributes[climate.ATTR_MIN_TEMP]
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
domain = entity.domain
min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
max_temp = entity.attributes[MIN_MAX_TEMP[domain]["max_temp"]]
unit = hass.config.units.temperature_unit
temp_delta = temperature_from_object(
@ -932,9 +956,11 @@ async def async_api_adjust_target_temp(
}
)
service = SERVICE_SET_TEMPERATURE[domain]
await hass.services.async_call(
entity.domain,
climate.SERVICE_SET_TEMPERATURE,
service,
data,
blocking=False,
context=context,
@ -1163,6 +1189,23 @@ 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]
@ -1174,6 +1217,15 @@ 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)
@ -1224,15 +1276,22 @@ async def async_api_toggle_on(
instance = directive.instance
domain = entity.domain
# Fan Oscillating
if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
data: dict[str, Any]
service = fan.SERVICE_OSCILLATE
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
fan.ATTR_OSCILLATING: True,
}
# 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:
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
await hass.services.async_call(
domain, service, data, blocking=False, context=context
@ -1375,6 +1434,17 @@ 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)
@ -1520,6 +1590,21 @@ 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)

View File

@ -4,7 +4,6 @@ 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"

View File

@ -1,5 +1,6 @@
"""Config flow for Ambiclimate."""
import logging
from typing import Any
from aiohttp import web
import ambiclimate
@ -7,7 +8,8 @@ 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 callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.network import get_url
from homeassistant.helpers.storage import Store
@ -26,7 +28,9 @@ _LOGGER = logging.getLogger(__name__)
@callback
def register_flow_implementation(hass, client_id, client_secret):
def register_flow_implementation(
hass: HomeAssistant, client_id: str, client_secret: str
) -> None:
"""Register a ambiclimate implementation.
client_id: Client id.
@ -50,7 +54,9 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._registered_view = False
self._oauth = None
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle external yaml configuration."""
self._async_abort_entries_match()
@ -62,7 +68,9 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_auth()
async def async_step_auth(self, user_input=None):
async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow start."""
self._async_abort_entries_match()
@ -83,7 +91,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_code(self, code=None):
async def async_step_code(self, code: str | None = None) -> FlowResult:
"""Received code for authentication."""
self._async_abort_entries_match()
@ -95,7 +103,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title="Ambiclimate", data=config)
async def _get_token_info(self, code):
async def _get_token_info(self, code: str | None) -> dict[str, Any] | None:
oauth = self._generate_oauth()
try:
token_info = await oauth.get_access_token(code)
@ -103,16 +111,16 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Failed to get access token")
return None
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
store = Store[dict[str, Any]](self.hass, STORAGE_VERSION, STORAGE_KEY)
await store.async_save(token_info)
return token_info
def _generate_view(self):
def _generate_view(self) -> None:
self.hass.http.register_view(AmbiclimateAuthCallbackView())
self._registered_view = True
def _generate_oauth(self):
def _generate_oauth(self) -> ambiclimate.AmbiclimateOAuth:
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
clientsession = async_get_clientsession(self.hass)
callback_url = self._cb_url()

View File

@ -63,14 +63,14 @@ TYPE_RELAY8 = "relay8"
TYPE_RELAY9 = "relay9"
@dataclass
@dataclass(frozen=True)
class AmbientBinarySensorDescriptionMixin:
"""Define an entity description mixin for binary sensors."""
on_state: Literal[0, 1]
@dataclass
@dataclass(frozen=True)
class AmbientBinarySensorDescription(
BinarySensorEntityDescription, AmbientBinarySensorDescriptionMixin
):

View File

@ -35,7 +35,7 @@ if TYPE_CHECKING:
from . import AmcrestDevice
@dataclass
@dataclass(frozen=True)
class AmcrestSensorEntityDescription(BinarySensorEntityDescription):
"""Describe Amcrest sensor entity."""

View File

@ -23,14 +23,14 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator
from .entity import AndroidIPCamBaseEntity
@dataclass
@dataclass(frozen=True)
class AndroidIPWebcamSensorEntityDescriptionMixin:
"""Mixin for required keys."""
value_fn: Callable[[PyDroidIPCam], StateType]
@dataclass
@dataclass(frozen=True)
class AndroidIPWebcamSensorEntityDescription(
SensorEntityDescription, AndroidIPWebcamSensorEntityDescriptionMixin
):

View File

@ -18,7 +18,7 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator
from .entity import AndroidIPCamBaseEntity
@dataclass
@dataclass(frozen=True)
class AndroidIPWebcamSwitchEntityDescriptionMixin:
"""Mixin for required keys."""
@ -26,7 +26,7 @@ class AndroidIPWebcamSwitchEntityDescriptionMixin:
off_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]]
@dataclass
@dataclass(frozen=True)
class AndroidIPWebcamSwitchEntityDescription(
SwitchEntityDescription, AndroidIPWebcamSwitchEntityDescriptionMixin
):

View File

@ -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."""

View File

@ -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 HomeAssistant, callback
from homeassistant.core import Event, 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) -> None:
def on_hass_stop(event: Event) -> None:
"""Stop push updates when hass stops."""
api.disconnect()

View File

@ -23,14 +23,14 @@ from .entity import AnovaDescriptionEntity
from .models import AnovaData
@dataclass
@dataclass(frozen=True)
class AnovaSensorEntityDescriptionMixin:
"""Describes the mixin variables for anova sensors."""
value_fn: Callable[[APCUpdateSensor], float | int | str]
@dataclass
@dataclass(frozen=True)
class AnovaSensorEntityDescription(
SensorEntityDescription, AnovaSensorEntityDescriptionMixin
):

View File

@ -10,18 +10,12 @@ 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_PORT
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, 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 (
CONF_MODEL,
DEFAULT_NAME,
DEFAULT_PORT,
DEVICE_TIMEOUT_SECONDS,
DOMAIN,
)
from .const import DEFAULT_NAME, DEFAULT_PORT, DEVICE_TIMEOUT_SECONDS, DOMAIN
_LOGGER = logging.getLogger(__name__)

View File

@ -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"

View File

@ -13,13 +13,13 @@ from homeassistant.components.media_player import (
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.const import CONF_MAC, CONF_MODEL
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, CONF_MODEL, DOMAIN, MANUFACTURER
from .const import ANTHEMAV_UPDATE_SIGNAL, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,73 @@
"""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

View File

@ -0,0 +1,107 @@
"""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,
)

View File

@ -0,0 +1,25 @@
"""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",
}

View File

@ -0,0 +1,83 @@
"""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

View File

@ -0,0 +1,62 @@
"""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)

View File

@ -0,0 +1,9 @@
{
"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"]
}

View File

@ -0,0 +1,106 @@
"""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

View File

@ -0,0 +1,43 @@
{
"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"
}
}
}
}

View File

@ -0,0 +1,151 @@
"""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()

View File

@ -7,8 +7,9 @@ from datetime import timedelta
import logging
from typing import Final
from apcaccess import status
import aioapcaccess
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
@ -32,6 +33,8 @@ 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__(
@ -70,13 +73,10 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]):
return self.data.get("SERIALNO")
@property
def device_info(self) -> DeviceInfo | None:
def device_info(self) -> DeviceInfo:
"""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)},
identifiers={(DOMAIN, self.ups_serial_no or self.config_entry.entry_id)},
model=self.ups_model,
manufacturer="APC",
name=self.ups_name if self.ups_name else "APC UPS",
@ -90,13 +90,8 @@ 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:
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:
return await aioapcaccess.request_status(self._host, self._port)
except (OSError, asyncio.IncompleteReadError) as error:
raise UpdateFailed(error) from error

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["apcaccess"],
"quality_scale": "silver",
"requirements": ["apcaccess==0.0.13"]
"requirements": ["aioapcaccess==0.4.2"]
}

View File

@ -0,0 +1 @@
"""Virtual integration: Appalachian Power."""

View File

@ -0,0 +1,6 @@
{
"domain": "appalachianpower",
"name": "Appalachian Power",
"integration_type": "virtual",
"supported_by": "opower"
}

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import logging
from typing import Any
import apprise
import voluptuous as vol
@ -61,11 +62,11 @@ def get_service(
class AppriseNotificationService(BaseNotificationService):
"""Implement the notification service for Apprise."""
def __init__(self, a_obj):
def __init__(self, a_obj: apprise.Apprise) -> None:
"""Initialize the service."""
self.apprise = a_obj
def send_message(self, message="", **kwargs):
def send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a specified target.
If no target/tags are specified, then services are notified as is

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN, UPDATE_TOPIC, AquaLogicProcessor
@dataclass
@dataclass(frozen=True)
class AquaLogicSensorEntityDescription(SensorEntityDescription):
"""Describes AquaLogic sensor entity."""

View File

@ -39,7 +39,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
@dataclass
@dataclass(frozen=True)
class AranetSensorEntityDescription(SensorEntityDescription):
"""Class to describe an Aranet sensor entity."""

View File

@ -20,14 +20,14 @@ from .coordinator import AsekoDataUpdateCoordinator
from .entity import AsekoEntity
@dataclass
@dataclass(frozen=True)
class AsekoBinarySensorDescriptionMixin:
"""Mixin for required keys."""
value_fn: Callable[[Unit], bool]
@dataclass
@dataclass(frozen=True)
class AsekoBinarySensorEntityDescription(
BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin
):

View File

@ -31,6 +31,7 @@ from .pipeline import (
async_get_pipeline,
async_get_pipelines,
async_setup_pipeline_store,
async_update_pipeline,
)
from .websocket_api import async_register_websocket_api
@ -40,6 +41,7 @@ __all__ = (
"async_get_pipelines",
"async_setup",
"async_pipeline_from_audio_stream",
"async_update_pipeline",
"AudioSettings",
"Pipeline",
"PipelineEvent",

View File

@ -43,6 +43,7 @@ from homeassistant.helpers.collection import (
)
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import (
dt as dt_util,
language as language_util,
@ -115,6 +116,7 @@ async def _async_resolve_default_pipeline_settings(
hass: HomeAssistant,
stt_engine_id: str | None,
tts_engine_id: str | None,
pipeline_name: str,
) -> dict[str, str | None]:
"""Resolve settings for a default pipeline.
@ -123,7 +125,6 @@ async def _async_resolve_default_pipeline_settings(
"""
conversation_language = "en"
pipeline_language = "en"
pipeline_name = "Home Assistant"
stt_engine = None
stt_language = None
tts_engine = None
@ -195,9 +196,6 @@ async def _async_resolve_default_pipeline_settings(
)
tts_engine_id = None
if stt_engine_id == "cloud" and tts_engine_id == "cloud":
pipeline_name = "Home Assistant Cloud"
return {
"conversation_engine": conversation.HOME_ASSISTANT_AGENT,
"conversation_language": conversation_language,
@ -221,12 +219,17 @@ async def _async_create_default_pipeline(
The default pipeline will use the homeassistant conversation agent and the
default stt / tts engines.
"""
pipeline_settings = await _async_resolve_default_pipeline_settings(hass, None, None)
pipeline_settings = await _async_resolve_default_pipeline_settings(
hass, stt_engine_id=None, tts_engine_id=None, pipeline_name="Home Assistant"
)
return await pipeline_store.async_create_item(pipeline_settings)
async def async_create_default_pipeline(
hass: HomeAssistant, stt_engine_id: str, tts_engine_id: str
hass: HomeAssistant,
stt_engine_id: str,
tts_engine_id: str,
pipeline_name: str,
) -> Pipeline | None:
"""Create a pipeline with default settings.
@ -236,7 +239,7 @@ async def async_create_default_pipeline(
pipeline_data: PipelineData = hass.data[DOMAIN]
pipeline_store = pipeline_data.pipeline_store
pipeline_settings = await _async_resolve_default_pipeline_settings(
hass, stt_engine_id, tts_engine_id
hass, stt_engine_id, tts_engine_id, pipeline_name=pipeline_name
)
if (
pipeline_settings["stt_engine"] != stt_engine_id
@ -274,6 +277,48 @@ def async_get_pipelines(hass: HomeAssistant) -> Iterable[Pipeline]:
return pipeline_data.pipeline_store.data.values()
async def async_update_pipeline(
hass: HomeAssistant,
pipeline: Pipeline,
*,
conversation_engine: str | UndefinedType = UNDEFINED,
conversation_language: str | UndefinedType = UNDEFINED,
language: str | UndefinedType = UNDEFINED,
name: str | UndefinedType = UNDEFINED,
stt_engine: str | None | UndefinedType = UNDEFINED,
stt_language: str | None | UndefinedType = UNDEFINED,
tts_engine: str | None | UndefinedType = UNDEFINED,
tts_language: str | None | UndefinedType = UNDEFINED,
tts_voice: str | None | UndefinedType = UNDEFINED,
wake_word_entity: str | None | UndefinedType = UNDEFINED,
wake_word_id: str | None | UndefinedType = UNDEFINED,
) -> None:
"""Update a pipeline."""
pipeline_data: PipelineData = hass.data[DOMAIN]
updates: dict[str, Any] = pipeline.to_json()
updates.pop("id")
# Refactor this once we bump to Python 3.12
# and have https://peps.python.org/pep-0692/
for key, val in (
("conversation_engine", conversation_engine),
("conversation_language", conversation_language),
("language", language),
("name", name),
("stt_engine", stt_engine),
("stt_language", stt_language),
("tts_engine", tts_engine),
("tts_language", tts_language),
("tts_voice", tts_voice),
("wake_word_entity", wake_word_entity),
("wake_word_id", wake_word_id),
):
if val is not UNDEFINED:
updates[key] = val
await pipeline_data.pipeline_store.async_update_item(pipeline.id, updates)
class PipelineEventType(StrEnum):
"""Event types emitted during a pipeline run."""

View File

@ -41,6 +41,7 @@ from .const import (
SENSORS_LOAD_AVG,
SENSORS_RATES,
SENSORS_TEMPERATURES,
SENSORS_TEMPERATURES_LEGACY,
)
SENSORS_TYPE_BYTES = "sensors_bytes"
@ -277,7 +278,7 @@ class AsusWrtLegacyBridge(AsusWrtBridge):
async def _get_available_temperature_sensors(self) -> list[str]:
"""Check which temperature information is available on the router."""
availability = await self._api.async_find_temperature_commands()
return [SENSORS_TEMPERATURES[i] for i in range(3) if availability[i]]
return [SENSORS_TEMPERATURES_LEGACY[i] for i in range(3) if availability[i]]
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
async def _get_bytes(self) -> Any:

View File

@ -30,4 +30,5 @@ SENSORS_BYTES = ["sensor_rx_bytes", "sensor_tx_bytes"]
SENSORS_CONNECTED_DEVICE = ["sensor_connected_device"]
SENSORS_LOAD_AVG = ["sensor_load_avg1", "sensor_load_avg5", "sensor_load_avg15"]
SENSORS_RATES = ["sensor_rx_rates", "sensor_tx_rates"]
SENSORS_TEMPERATURES = ["2.4GHz", "5.0GHz", "CPU"]
SENSORS_TEMPERATURES_LEGACY = ["2.4GHz", "5.0GHz", "CPU"]
SENSORS_TEMPERATURES = [*SENSORS_TEMPERATURES_LEGACY, "5.0GHz_2", "6.0GHz"]

View File

@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asyncssh"],
"requirements": ["aioasuswrt==1.4.0", "pyasuswrt==0.1.20"]
"requirements": ["aioasuswrt==1.4.0", "pyasuswrt==0.1.21"]
}

View File

@ -38,7 +38,7 @@ from .const import (
from .router import AsusWrtRouter
@dataclass
@dataclass(frozen=True)
class AsusWrtSensorEntityDescription(SensorEntityDescription):
"""A class that describes AsusWrt sensor entities."""
@ -156,6 +156,26 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = (
entity_registry_enabled_default=False,
suggested_display_precision=1,
),
AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[3],
translation_key="5ghz_2_temperature",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
suggested_display_precision=1,
),
AsusWrtSensorEntityDescription(
key=SENSORS_TEMPERATURES[4],
translation_key="6ghz_temperature",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
suggested_display_precision=1,
),
)

View File

@ -82,6 +82,12 @@
},
"cpu_temperature": {
"name": "CPU Temperature"
},
"5ghz_2_temperature": {
"name": "5GHz Temperature (Radio 2)"
},
"6ghz_temperature": {
"name": "6GHz Temperature"
}
}
},

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