mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 22:07:10 +00:00
2024.1.0 (#106970)
This commit is contained in:
commit
fb0cc6c5d0
43
.coveragerc
43
.coveragerc
@ -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/*
|
||||
|
8
.github/workflows/builder.yml
vendored
8
.github/workflows/builder.yml
vendored
@ -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"
|
||||
|
||||
|
26
.github/workflows/ci.yaml
vendored
26
.github/workflows/ci.yaml
vendored
@ -36,7 +36,7 @@ env:
|
||||
CACHE_VERSION: 5
|
||||
PIP_CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 6
|
||||
HA_SHORT_VERSION: "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
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -29,11 +29,11 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@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"
|
||||
|
18
.github/workflows/stale.yml
vendored
18
.github/workflows/stale.yml
vendored
@ -11,16 +11,16 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# The 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"
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.7.1
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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.*
|
||||
|
62
CODEOWNERS
62
CODEOWNERS
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
5
homeassistant/brands/flexit.json
Normal file
5
homeassistant/brands/flexit.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "flexit",
|
||||
"name": "Flexit",
|
||||
"integrations": ["flexit", "flexit_bacnet"]
|
||||
}
|
@ -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."""
|
||||
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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."""
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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 = {}
|
||||
|
||||
|
1
homeassistant/components/aep_ohio/__init__.py
Normal file
1
homeassistant/components/aep_ohio/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Virtual integration: AEP Ohio."""
|
6
homeassistant/components/aep_ohio/manifest.json
Normal file
6
homeassistant/components/aep_ohio/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "aep_ohio",
|
||||
"name": "AEP Ohio",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
}
|
1
homeassistant/components/aep_texas/__init__.py
Normal file
1
homeassistant/components/aep_texas/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Virtual integration: AEP Texas."""
|
6
homeassistant/components/aep_texas/manifest.json
Normal file
6
homeassistant/components/aep_texas/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "aep_texas",
|
||||
"name": "AEP Texas",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
}
|
@ -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)
|
||||
|
@ -56,7 +56,7 @@ from .const import (
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class AirlySensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Airly sensor entities."""
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.3.1"]
|
||||
"requirements": ["aioairq==0.3.2"]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -2,5 +2,4 @@
|
||||
|
||||
DOMAIN = "airthings"
|
||||
|
||||
CONF_ID = "id"
|
||||
CONF_SECRET = "secret"
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -34,7 +34,7 @@ from .entity import (
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""A class that describes Airzone Cloud binary sensor entities."""
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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 {}
|
||||
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
||||
from . import AmcrestDevice
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class AmcrestSensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describe Amcrest sensor entity."""
|
||||
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -160,7 +160,7 @@ def adb_decorator(
|
||||
"""
|
||||
|
||||
def _adb_decorator(
|
||||
func: _FuncType[_ADBDeviceT, _P, _R]
|
||||
func: _FuncType[_ADBDeviceT, _P, _R],
|
||||
) -> _ReturnFuncType[_ADBDeviceT, _P, _R]:
|
||||
"""Wrap the provided ADB method and catch exceptions."""
|
||||
|
||||
|
@ -14,7 +14,7 @@ from androidtvremote2 import (
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import 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()
|
||||
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Constants for the Anthem A/V Receivers integration."""
|
||||
ANTHEMAV_UPDATE_SIGNAL = "anthemav_update"
|
||||
CONF_MODEL = "model"
|
||||
|
||||
DEFAULT_NAME = "Anthem AV"
|
||||
DEFAULT_PORT = 14999
|
||||
DOMAIN = "anthemav"
|
||||
|
@ -13,13 +13,13 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MAC
|
||||
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__)
|
||||
|
||||
|
73
homeassistant/components/aosmith/__init__.py
Normal file
73
homeassistant/components/aosmith/__init__.py
Normal 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
|
107
homeassistant/components/aosmith/config_flow.py
Normal file
107
homeassistant/components/aosmith/config_flow.py
Normal 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,
|
||||
)
|
25
homeassistant/components/aosmith/const.py
Normal file
25
homeassistant/components/aosmith/const.py
Normal 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",
|
||||
}
|
83
homeassistant/components/aosmith/coordinator.py
Normal file
83
homeassistant/components/aosmith/coordinator.py
Normal 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
|
62
homeassistant/components/aosmith/entity.py
Normal file
62
homeassistant/components/aosmith/entity.py
Normal 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)
|
9
homeassistant/components/aosmith/manifest.json
Normal file
9
homeassistant/components/aosmith/manifest.json
Normal 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"]
|
||||
}
|
106
homeassistant/components/aosmith/sensor.py
Normal file
106
homeassistant/components/aosmith/sensor.py
Normal 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
|
43
homeassistant/components/aosmith/strings.json
Normal file
43
homeassistant/components/aosmith/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
151
homeassistant/components/aosmith/water_heater.py
Normal file
151
homeassistant/components/aosmith/water_heater.py
Normal 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()
|
@ -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
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["apcaccess"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["apcaccess==0.0.13"]
|
||||
"requirements": ["aioapcaccess==0.4.2"]
|
||||
}
|
||||
|
1
homeassistant/components/appalachianpower/__init__.py
Normal file
1
homeassistant/components/appalachianpower/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Virtual integration: Appalachian Power."""
|
6
homeassistant/components/appalachianpower/manifest.json
Normal file
6
homeassistant/components/appalachianpower/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "appalachianpower",
|
||||
"name": "Appalachian Power",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
}
|
@ -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
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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",
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user