Compare commits

..

3 Commits

Author SHA1 Message Date
c0ffeeca7
83458d24c7 Implement review comments from #104658 2023-11-29 05:36:29 +00:00
c0ffeeca7
df6d43adc4 Fix style 2023-11-28 17:44:45 +00:00
c0ffeeca7
5cbfc1c224 Add info what to enter into host field 2023-11-28 17:35:21 +00:00
4422 changed files with 47297 additions and 205899 deletions

View File

@@ -28,6 +28,7 @@ omit =
homeassistant/components/adguard/sensor.py
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/aemet/weather_update_coordinator.py
homeassistant/components/aftership/__init__.py
homeassistant/components/aftership/sensor.py
homeassistant/components/agent_dvr/alarm_control_panel.py
@@ -46,9 +47,6 @@ omit =
homeassistant/components/airtouch4/__init__.py
homeassistant/components/airtouch4/climate.py
homeassistant/components/airtouch4/coordinator.py
homeassistant/components/airtouch5/__init__.py
homeassistant/components/airtouch5/climate.py
homeassistant/components/airtouch5/entity.py
homeassistant/components/airvisual/__init__.py
homeassistant/components/airvisual/sensor.py
homeassistant/components/airvisual_pro/__init__.py
@@ -112,12 +110,6 @@ omit =
homeassistant/components/baf/sensor.py
homeassistant/components/baf/switch.py
homeassistant/components/baidu/tts.py
homeassistant/components/bang_olufsen/__init__.py
homeassistant/components/bang_olufsen/const.py
homeassistant/components/bang_olufsen/entity.py
homeassistant/components/bang_olufsen/media_player.py
homeassistant/components/bang_olufsen/util.py
homeassistant/components/bang_olufsen/websocket.py
homeassistant/components/bbox/device_tracker.py
homeassistant/components/bbox/sensor.py
homeassistant/components/beewi_smartclim/sensor.py
@@ -150,8 +142,6 @@ omit =
homeassistant/components/braviatv/coordinator.py
homeassistant/components/braviatv/media_player.py
homeassistant/components/braviatv/remote.py
homeassistant/components/bring/coordinator.py
homeassistant/components/bring/todo.py
homeassistant/components/broadlink/climate.py
homeassistant/components/broadlink/light.py
homeassistant/components/broadlink/remote.py
@@ -183,8 +173,6 @@ omit =
homeassistant/components/coinbase/sensor.py
homeassistant/components/comed_hourly_pricing/sensor.py
homeassistant/components/comelit/__init__.py
homeassistant/components/comelit/alarm_control_panel.py
homeassistant/components/comelit/climate.py
homeassistant/components/comelit/const.py
homeassistant/components/comelit/cover.py
homeassistant/components/comelit/coordinator.py
@@ -284,12 +272,7 @@ omit =
homeassistant/components/econet/climate.py
homeassistant/components/econet/sensor.py
homeassistant/components/econet/water_heater.py
homeassistant/components/ecovacs/controller.py
homeassistant/components/ecovacs/entity.py
homeassistant/components/ecovacs/image.py
homeassistant/components/ecovacs/number.py
homeassistant/components/ecovacs/util.py
homeassistant/components/ecovacs/vacuum.py
homeassistant/components/ecovacs/*
homeassistant/components/ecowitt/__init__.py
homeassistant/components/ecowitt/binary_sensor.py
homeassistant/components/ecowitt/entity.py
@@ -320,8 +303,6 @@ omit =
homeassistant/components/elmax/cover.py
homeassistant/components/elmax/switch.py
homeassistant/components/elv/*
homeassistant/components/elvia/__init__.py
homeassistant/components/elvia/importer.py
homeassistant/components/emby/media_player.py
homeassistant/components/emoncms/sensor.py
homeassistant/components/emoncms_history/*
@@ -350,15 +331,13 @@ omit =
homeassistant/components/environment_canada/weather.py
homeassistant/components/envisalink/*
homeassistant/components/ephember/climate.py
homeassistant/components/epion/__init__.py
homeassistant/components/epion/coordinator.py
homeassistant/components/epion/sensor.py
homeassistant/components/epson/__init__.py
homeassistant/components/epson/media_player.py
homeassistant/components/epsonworkforce/sensor.py
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/*
@@ -385,6 +364,8 @@ omit =
homeassistant/components/faa_delays/binary_sensor.py
homeassistant/components/faa_delays/coordinator.py
homeassistant/components/familyhub/camera.py
homeassistant/components/fastdotcom/sensor.py
homeassistant/components/fastdotcom/__init__.py
homeassistant/components/ffmpeg/camera.py
homeassistant/components/fibaro/__init__.py
homeassistant/components/fibaro/binary_sensor.py
@@ -423,7 +404,6 @@ omit =
homeassistant/components/fjaraskupan/sensor.py
homeassistant/components/fleetgo/device_tracker.py
homeassistant/components/flexit/climate.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
@@ -439,8 +419,6 @@ omit =
homeassistant/components/fortios/device_tracker.py
homeassistant/components/foscam/__init__.py
homeassistant/components/foscam/camera.py
homeassistant/components/foscam/coordinator.py
homeassistant/components/foscam/entity.py
homeassistant/components/foursquare/*
homeassistant/components/free_mobile/notify.py
homeassistant/components/freebox/camera.py
@@ -481,7 +459,6 @@ omit =
homeassistant/components/google_cloud/tts.py
homeassistant/components/google_maps/device_tracker.py
homeassistant/components/google_pubsub/__init__.py
homeassistant/components/gpsd/__init__.py
homeassistant/components/gpsd/sensor.py
homeassistant/components/greenwave/light.py
homeassistant/components/growatt_server/__init__.py
@@ -492,11 +469,9 @@ omit =
homeassistant/components/guardian/__init__.py
homeassistant/components/guardian/binary_sensor.py
homeassistant/components/guardian/button.py
homeassistant/components/guardian/coordinator.py
homeassistant/components/guardian/sensor.py
homeassistant/components/guardian/switch.py
homeassistant/components/guardian/util.py
homeassistant/components/guardian/valve.py
homeassistant/components/habitica/__init__.py
homeassistant/components/habitica/sensor.py
homeassistant/components/harman_kardon_avr/media_player.py
@@ -518,9 +493,6 @@ omit =
homeassistant/components/hive/sensor.py
homeassistant/components/hive/switch.py
homeassistant/components/hive/water_heater.py
homeassistant/components/hko/__init__.py
homeassistant/components/hko/weather.py
homeassistant/components/hko/coordinator.py
homeassistant/components/hlk_sw16/__init__.py
homeassistant/components/hlk_sw16/switch.py
homeassistant/components/home_connect/__init__.py
@@ -530,6 +502,8 @@ omit =
homeassistant/components/home_connect/light.py
homeassistant/components/home_connect/sensor.py
homeassistant/components/home_connect/switch.py
homeassistant/components/home_plus_control/api.py
homeassistant/components/home_plus_control/switch.py
homeassistant/components/homematic/__init__.py
homeassistant/components/homematic/binary_sensor.py
homeassistant/components/homematic/climate.py
@@ -559,12 +533,9 @@ omit =
homeassistant/components/hunterdouglas_powerview/shade_data.py
homeassistant/components/hunterdouglas_powerview/util.py
homeassistant/components/hvv_departures/__init__.py
homeassistant/components/huum/__init__.py
homeassistant/components/huum/climate.py
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
@@ -662,6 +633,8 @@ omit =
homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/media_player.py
homeassistant/components/kodi/notify.py
homeassistant/components/komfovent/__init__.py
homeassistant/components/komfovent/climate.py
homeassistant/components/konnected/__init__.py
homeassistant/components/konnected/panel.py
homeassistant/components/konnected/switch.py
@@ -687,6 +660,10 @@ omit =
homeassistant/components/lg_netcast/media_player.py
homeassistant/components/lg_soundbar/__init__.py
homeassistant/components/lg_soundbar/media_player.py
homeassistant/components/life360/__init__.py
homeassistant/components/life360/button.py
homeassistant/components/life360/coordinator.py
homeassistant/components/life360/device_tracker.py
homeassistant/components/lightwave/*
homeassistant/components/limitlessled/light.py
homeassistant/components/linksys_smart/device_tracker.py
@@ -714,16 +691,10 @@ omit =
homeassistant/components/loqed/sensor.py
homeassistant/components/luci/device_tracker.py
homeassistant/components/luftdaten/sensor.py
homeassistant/components/lupusec/__init__.py
homeassistant/components/lupusec/alarm_control_panel.py
homeassistant/components/lupusec/binary_sensor.py
homeassistant/components/lupusec/entity.py
homeassistant/components/lupusec/switch.py
homeassistant/components/lupusec/*
homeassistant/components/lutron/__init__.py
homeassistant/components/lutron/binary_sensor.py
homeassistant/components/lutron/cover.py
homeassistant/components/lutron/entity.py
homeassistant/components/lutron/fan.py
homeassistant/components/lutron/light.py
homeassistant/components/lutron/switch.py
homeassistant/components/lutron_caseta/__init__.py
@@ -785,12 +756,6 @@ omit =
homeassistant/components/motion_blinds/cover.py
homeassistant/components/motion_blinds/entity.py
homeassistant/components/motion_blinds/sensor.py
homeassistant/components/motionmount/__init__.py
homeassistant/components/motionmount/binary_sensor.py
homeassistant/components/motionmount/entity.py
homeassistant/components/motionmount/number.py
homeassistant/components/motionmount/select.py
homeassistant/components/motionmount/sensor.py
homeassistant/components/mpd/media_player.py
homeassistant/components/mqtt_room/sensor.py
homeassistant/components/msteams/notify.py
@@ -836,8 +801,7 @@ omit =
homeassistant/components/netgear/sensor.py
homeassistant/components/netgear/switch.py
homeassistant/components/netgear/update.py
homeassistant/components/netgear_lte/__init__.py
homeassistant/components/netgear_lte/notify.py
homeassistant/components/netgear_lte/*
homeassistant/components/netio/switch.py
homeassistant/components/neurio_energy/sensor.py
homeassistant/components/nexia/climate.py
@@ -848,7 +812,6 @@ omit =
homeassistant/components/nextcloud/coordinator.py
homeassistant/components/nextcloud/entity.py
homeassistant/components/nextcloud/sensor.py
homeassistant/components/nextcloud/update.py
homeassistant/components/nfandroidtv/__init__.py
homeassistant/components/nfandroidtv/notify.py
homeassistant/components/nibe_heatpump/__init__.py
@@ -939,9 +902,6 @@ omit =
homeassistant/components/opple/light.py
homeassistant/components/oru/*
homeassistant/components/orvibo/switch.py
homeassistant/components/osoenergy/__init__.py
homeassistant/components/osoenergy/const.py
homeassistant/components/osoenergy/water_heater.py
homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py
homeassistant/components/overkiz/__init__.py
@@ -1031,11 +991,6 @@ omit =
homeassistant/components/qrcode/image_processing.py
homeassistant/components/quantum_gateway/device_tracker.py
homeassistant/components/qvr_pro/*
homeassistant/components/rabbitair/__init__.py
homeassistant/components/rabbitair/const.py
homeassistant/components/rabbitair/coordinator.py
homeassistant/components/rabbitair/entity.py
homeassistant/components/rabbitair/fan.py
homeassistant/components/rachio/__init__.py
homeassistant/components/rachio/binary_sensor.py
homeassistant/components/rachio/device.py
@@ -1068,18 +1023,11 @@ omit =
homeassistant/components/renson/fan.py
homeassistant/components/renson/binary_sensor.py
homeassistant/components/renson/number.py
homeassistant/components/renson/time.py
homeassistant/components/raspyrfm/*
homeassistant/components/recollect_waste/sensor.py
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/*
@@ -1107,9 +1055,6 @@ omit =
homeassistant/components/ripple/sensor.py
homeassistant/components/roborock/coordinator.py
homeassistant/components/rocketchat/notify.py
homeassistant/components/romy/__init__.py
homeassistant/components/romy/coordinator.py
homeassistant/components/romy/vacuum.py
homeassistant/components/roomba/__init__.py
homeassistant/components/roomba/binary_sensor.py
homeassistant/components/roomba/braava.py
@@ -1266,7 +1211,6 @@ omit =
homeassistant/components/starline/__init__.py
homeassistant/components/starline/account.py
homeassistant/components/starline/binary_sensor.py
homeassistant/components/starline/button.py
homeassistant/components/starline/device_tracker.py
homeassistant/components/starline/entity.py
homeassistant/components/starline/lock.py
@@ -1284,12 +1228,8 @@ omit =
homeassistant/components/stream/fmp4utils.py
homeassistant/components/stream/hls.py
homeassistant/components/stream/worker.py
homeassistant/components/streamlabswater/__init__.py
homeassistant/components/streamlabswater/binary_sensor.py
homeassistant/components/streamlabswater/coordinator.py
homeassistant/components/streamlabswater/sensor.py
homeassistant/components/suez_water/__init__.py
homeassistant/components/suez_water/sensor.py
homeassistant/components/streamlabswater/*
homeassistant/components/suez_water/*
homeassistant/components/supervisord/sensor.py
homeassistant/components/supla/*
homeassistant/components/surepetcare/__init__.py
@@ -1297,8 +1237,6 @@ omit =
homeassistant/components/surepetcare/entity.py
homeassistant/components/surepetcare/sensor.py
homeassistant/components/swiss_hydrological_data/sensor.py
homeassistant/components/swiss_public_transport/__init__.py
homeassistant/components/swiss_public_transport/coordinator.py
homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbee/__init__.py
@@ -1350,13 +1288,13 @@ omit =
homeassistant/components/system_bridge/notify.py
homeassistant/components/system_bridge/sensor.py
homeassistant/components/system_bridge/update.py
homeassistant/components/systemmonitor/sensor.py
homeassistant/components/tado/__init__.py
homeassistant/components/tado/binary_sensor.py
homeassistant/components/tado/climate.py
homeassistant/components/tado/device_tracker.py
homeassistant/components/tado/sensor.py
homeassistant/components/tado/water_heater.py
homeassistant/components/tami4/button.py
homeassistant/components/tank_utility/sensor.py
homeassistant/components/tankerkoenig/__init__.py
homeassistant/components/tankerkoenig/binary_sensor.py
@@ -1425,11 +1363,6 @@ omit =
homeassistant/components/tplink_omada/controller.py
homeassistant/components/tplink_omada/update.py
homeassistant/components/traccar/device_tracker.py
homeassistant/components/traccar_server/__init__.py
homeassistant/components/traccar_server/coordinator.py
homeassistant/components/traccar_server/device_tracker.py
homeassistant/components/traccar_server/entity.py
homeassistant/components/traccar_server/helpers.py
homeassistant/components/tractive/__init__.py
homeassistant/components/tractive/binary_sensor.py
homeassistant/components/tractive/device_tracker.py
@@ -1444,6 +1377,10 @@ omit =
homeassistant/components/tradfri/light.py
homeassistant/components/tradfri/sensor.py
homeassistant/components/tradfri/switch.py
homeassistant/components/trafikverket_train/__init__.py
homeassistant/components/trafikverket_train/coordinator.py
homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_train/util.py
homeassistant/components/trafikverket_weatherstation/__init__.py
homeassistant/components/trafikverket_weatherstation/coordinator.py
homeassistant/components/trafikverket_weatherstation/sensor.py
@@ -1478,8 +1415,6 @@ omit =
homeassistant/components/ukraine_alarm/__init__.py
homeassistant/components/ukraine_alarm/binary_sensor.py
homeassistant/components/unifiled/*
homeassistant/components/unifi_direct/__init__.py
homeassistant/components/unifi_direct/device_tracker.py
homeassistant/components/upb/__init__.py
homeassistant/components/upb/light.py
homeassistant/components/upc_connect/*
@@ -1529,6 +1464,7 @@ omit =
homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.py
homeassistant/components/vicare/__init__.py
homeassistant/components/vicare/binary_sensor.py
homeassistant/components/vicare/button.py
homeassistant/components/vicare/climate.py
homeassistant/components/vicare/entity.py
@@ -1648,9 +1584,7 @@ omit =
homeassistant/components/yolink/entity.py
homeassistant/components/yolink/light.py
homeassistant/components/yolink/lock.py
homeassistant/components/yolink/number.py
homeassistant/components/yolink/sensor.py
homeassistant/components/yolink/services.py
homeassistant/components/yolink/siren.py
homeassistant/components/yolink/switch.py
homeassistant/components/youless/__init__.py
@@ -1689,13 +1623,6 @@ omit =
homeassistant/components/zwave_me/switch.py
homeassistant/components/electrasmart/climate.py
homeassistant/components/electrasmart/__init__.py
homeassistant/components/myuplink/__init__.py
homeassistant/components/myuplink/api.py
homeassistant/components/myuplink/application_credentials.py
homeassistant/components/myuplink/coordinator.py
homeassistant/components/myuplink/entity.py
homeassistant/components/myuplink/sensor.py
[report]
# Regexes for lines to exclude from consideration

View File

@@ -5,8 +5,7 @@
"postCreateCommand": "script/setup",
"postStartCommand": "script/bootstrap",
"containerEnv": { "DEVCONTAINER": "1" },
// Port 5683 udp is used by Shelly integration
"appPort": ["8123:8123", "5683:5683/udp"],
"appPort": ["8123:8123"],
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
"customizations": {
"vscode": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -10,8 +10,7 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.12"
PIP_TIMEOUT: 60
DEFAULT_PYTHON: "3.11"
jobs:
init:
@@ -30,7 +29,7 @@ jobs:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -60,7 +59,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -103,7 +102,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v3.0.0
uses: dawidd6/action-download-artifact@v2
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -114,7 +113,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v3.0.0
uses: dawidd6/action-download-artifact@v2
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package
@@ -125,7 +124,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -180,15 +179,6 @@ jobs:
sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Adjustments for 64-bit
if: matrix.arch == 'amd64' || matrix.arch == 'aarch64'
run: |
# Some speedups are only available on 64-bit, and since
# we build 32bit images on 64bit hosts, we only enable
# the speed ups on 64bit since the wheels for 32bit
# are not available.
sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt
- name: Download Translations
run: python3 -m script.translations download
env:
@@ -207,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.01.0
uses: home-assistant/builder@2023.09.0
with:
args: |
$BUILD_ARGS \
@@ -257,7 +247,6 @@ jobs:
- raspberrypi3-64
- raspberrypi4
- raspberrypi4-64
- raspberrypi5-64
- tinker
- yellow
- green
@@ -284,7 +273,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.01.0
uses: home-assistant/builder@2023.09.0
with:
args: |
$BUILD_ARGS \
@@ -341,7 +330,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Install Cosign
uses: sigstore/cosign-installer@v3.3.0
uses: sigstore/cosign-installer@v3.2.0
with:
cosign-release: "v2.0.2"

View File

@@ -35,8 +35,8 @@ on:
env:
CACHE_VERSION: 5
PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 7
HA_SHORT_VERSION: "2024.2"
MYPY_CACHE_VERSION: 6
HA_SHORT_VERSION: "2023.12"
DEFAULT_PYTHON: "3.11"
ALL_PYTHON_VERSIONS: "['3.11', '3.12']"
# 10.3 is the oldest supported version
@@ -103,7 +103,7 @@ jobs:
echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{
hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Filter for core changes
uses: dorny/paths-filter@v3.0.0
uses: dorny/paths-filter@v2.11.1
id: core
with:
filters: .core_files.yaml
@@ -118,7 +118,7 @@ jobs:
echo "Result:"
cat .integration_paths.yaml
- name: Filter for integration changes
uses: dorny/paths-filter@v3.0.0
uses: dorny/paths-filter@v2.11.1
id: integrations
with:
filters: .integration_paths.yaml
@@ -225,13 +225,13 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.0.0
uses: actions/cache@v3.3.2
with:
path: venv
key: >-
@@ -246,7 +246,7 @@ jobs:
pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache@v4.0.0
uses: actions/cache@v3.3.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
@@ -269,14 +269,14 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -285,7 +285,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -309,14 +309,14 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -325,7 +325,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -348,14 +348,14 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -364,7 +364,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -443,7 +443,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -454,7 +454,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.0.0
uses: actions/cache@v3.3.2
with:
path: venv
lookup-only: true
@@ -463,7 +463,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore pip wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.0.0
uses: actions/cache@v3.3.2
with:
path: ${{ env.PIP_CACHE }}
key: >-
@@ -511,13 +511,13 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -543,13 +543,13 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -576,13 +576,13 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -597,14 +597,14 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant
pylint --ignore-missing-annotations=y homeassistant
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
python --version
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
mypy:
name: Check mypy
@@ -620,7 +620,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -633,7 +633,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -641,7 +641,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v4.0.0
uses: actions/cache@v3.3.2
with:
path: .mypy_cache
key: >-
@@ -702,13 +702,13 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -854,13 +854,13 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -898,7 +898,6 @@ jobs:
python --version
set -o pipefail
mariadb=$(echo "${{ matrix.mariadb-group }}" | sed "s/:/-/g")
echo "mariadb=${mariadb}" >> $GITHUB_OUTPUT
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.recorder")
@@ -930,8 +929,7 @@ jobs:
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.2
with:
name: coverage-${{ matrix.python-version }}-mariadb-${{
steps.pytest-partial.outputs.mariadb }}
name: coverage-${{ matrix.python-version }}-mariadb
path: coverage.xml
- name: Check dirty
run: |
@@ -980,13 +978,13 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.0.0
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.0
uses: actions/cache/restore@v3.3.2
with:
path: venv
fail-on-cache-miss: true
@@ -1024,7 +1022,6 @@ jobs:
python --version
set -o pipefail
postgresql=$(echo "${{ matrix.postgresql-group }}" | sed "s/:/-/g")
echo "postgresql=${postgresql}" >> $GITHUB_OUTPUT
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.recorder")
@@ -1057,8 +1054,7 @@ jobs:
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.0
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
name: coverage-${{ matrix.python-version }}-postgresql
path: coverage.xml
- name: Check dirty
run: |

View File

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

View File

@@ -11,16 +11,16 @@ jobs:
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
steps:
# The 60 day stale policy for PRs
# The 90 day stale policy for PRs
# Used for:
# - PRs
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
uses: actions/stale@v9.0.0
- name: 90 days stale PRs policy
uses: actions/stale@v8.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
days-before-stale: 90
days-before-close: 7
days-before-issue-stale: -1
days-before-issue-close: -1
@@ -33,11 +33,7 @@ jobs:
pull request has been automatically marked as stale because of that
and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want
to keep it open. Also, please rebase your PR onto the latest dev
branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
Thank you for your contributions.
# Generate a token for the GitHub App, we use this method to avoid
# hitting API limits for our GitHub actions + have a higher rate limit.
@@ -57,7 +53,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@v9.0.0
uses: actions/stale@v8.0.0
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@@ -87,7 +83,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@v9.0.0
uses: actions/stale@v8.0.0
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"

View File

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

View File

@@ -99,14 +99,14 @@ jobs:
name: requirements_diff
- name: Build wheels
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2023.10.5
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm"
apk: "libffi-dev;openssl-dev;yaml-dev"
skip-binary: aiohttp
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
@@ -160,12 +160,6 @@ jobs:
sed -i "s|pyezviz|# pyezviz|g" ${requirement_file}
sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file}
fi
# Some speedups are only for 64-bit
if [ "${{ matrix.arch }}" = "amd64" ] || [ "${{ matrix.arch }}" = "aarch64" ]; then
sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" ${requirement_file}
fi
done
- name: Split requirements all
@@ -198,7 +192,7 @@ jobs:
sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (old cython)
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2023.10.5
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -213,42 +207,42 @@ jobs:
pip: "'cython<3'"
- name: Build wheels (part 1)
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2023.10.5
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
- name: Build wheels (part 2)
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2023.10.5
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
- name: Build wheels (part 3)
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2023.10.5
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.8
rev: v0.1.6
hooks:
- id: ruff
args:
@@ -83,7 +83,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$
files: ^(homeassistant/.+/(manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata

View File

@@ -42,101 +42,59 @@ homeassistant.components
homeassistant.components.abode.*
homeassistant.components.accuweather.*
homeassistant.components.acer_projector.*
homeassistant.components.acmeda.*
homeassistant.components.actiontec.*
homeassistant.components.adax.*
homeassistant.components.adguard.*
homeassistant.components.aftership.*
homeassistant.components.air_quality.*
homeassistant.components.airly.*
homeassistant.components.airnow.*
homeassistant.components.airq.*
homeassistant.components.airthings.*
homeassistant.components.airthings_ble.*
homeassistant.components.airtouch5.*
homeassistant.components.airvisual.*
homeassistant.components.airvisual_pro.*
homeassistant.components.airzone.*
homeassistant.components.airzone_cloud.*
homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.*
homeassistant.components.ambiclimate.*
homeassistant.components.ambient_station.*
homeassistant.components.amcrest.*
homeassistant.components.ampio.*
homeassistant.components.analytics.*
homeassistant.components.analytics_insights.*
homeassistant.components.android_ip_webcam.*
homeassistant.components.androidtv.*
homeassistant.components.androidtv_remote.*
homeassistant.components.anel_pwrctrl.*
homeassistant.components.anova.*
homeassistant.components.anthemav.*
homeassistant.components.apache_kafka.*
homeassistant.components.apcupsd.*
homeassistant.components.api.*
homeassistant.components.apprise.*
homeassistant.components.aprs.*
homeassistant.components.aqualogic.*
homeassistant.components.aquostv.*
homeassistant.components.aranet.*
homeassistant.components.arcam_fmj.*
homeassistant.components.arris_tg2492lg.*
homeassistant.components.aruba.*
homeassistant.components.arwn.*
homeassistant.components.aseko_pool_live.*
homeassistant.components.assist_pipeline.*
homeassistant.components.asterisk_cdr.*
homeassistant.components.asterisk_mbox.*
homeassistant.components.asuswrt.*
homeassistant.components.auth.*
homeassistant.components.automation.*
homeassistant.components.awair.*
homeassistant.components.axis.*
homeassistant.components.backup.*
homeassistant.components.baf.*
homeassistant.components.bang_olufsen.*
homeassistant.components.bayesian.*
homeassistant.components.binary_sensor.*
homeassistant.components.bitcoin.*
homeassistant.components.blockchain.*
homeassistant.components.blue_current.*
homeassistant.components.blueprint.*
homeassistant.components.bluetooth.*
homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.*
homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.*
homeassistant.components.braviatv.*
homeassistant.components.brother.*
homeassistant.components.browser.*
homeassistant.components.bthome.*
homeassistant.components.button.*
homeassistant.components.calendar.*
homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.cert_expiry.*
homeassistant.components.clickatell.*
homeassistant.components.clicksend.*
homeassistant.components.climate.*
homeassistant.components.cloud.*
homeassistant.components.co2signal.*
homeassistant.components.command_line.*
homeassistant.components.config.*
homeassistant.components.configurator.*
homeassistant.components.counter.*
homeassistant.components.cover.*
homeassistant.components.cpuspeed.*
homeassistant.components.crownstone.*
homeassistant.components.date.*
homeassistant.components.datetime.*
homeassistant.components.deconz.*
homeassistant.components.default_config.*
homeassistant.components.demo.*
homeassistant.components.derivative.*
homeassistant.components.device_automation.*
@@ -147,18 +105,11 @@ homeassistant.components.dhcp.*
homeassistant.components.diagnostics.*
homeassistant.components.discovergy.*
homeassistant.components.dlna_dmr.*
homeassistant.components.dlna_dms.*
homeassistant.components.dnsip.*
homeassistant.components.doorbird.*
homeassistant.components.dormakaba_dkey.*
homeassistant.components.downloader.*
homeassistant.components.dsmr.*
homeassistant.components.duckdns.*
homeassistant.components.dunehd.*
homeassistant.components.duotecno.*
homeassistant.components.easyenergy.*
homeassistant.components.ecovacs.*
homeassistant.components.ecowitt.*
homeassistant.components.efergy.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
@@ -166,14 +117,9 @@ homeassistant.components.elgato.*
homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
homeassistant.components.energy.*
homeassistant.components.energyzero.*
homeassistant.components.enigma2.*
homeassistant.components.enphase_envoy.*
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.*
@@ -181,7 +127,6 @@ homeassistant.components.file_upload.*
homeassistant.components.filesize.*
homeassistant.components.filter.*
homeassistant.components.fitbit.*
homeassistant.components.flexit_bacnet.*
homeassistant.components.flux_led.*
homeassistant.components.forecast_solar.*
homeassistant.components.fritz.*
@@ -190,15 +135,12 @@ homeassistant.components.fritzbox_callmonitor.*
homeassistant.components.fronius.*
homeassistant.components.frontend.*
homeassistant.components.fully_kiosk.*
homeassistant.components.generic_hygrostat.*
homeassistant.components.generic_thermostat.*
homeassistant.components.geo_location.*
homeassistant.components.geocaching.*
homeassistant.components.gios.*
homeassistant.components.glances.*
homeassistant.components.goalzero.*
homeassistant.components.google.*
homeassistant.components.google_assistant_sdk.*
homeassistant.components.google_sheets.*
homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.*
@@ -208,9 +150,8 @@ homeassistant.components.hardkernel.*
homeassistant.components.hardware.*
homeassistant.components.here_travel_time.*
homeassistant.components.history.*
homeassistant.components.history_stats.*
homeassistant.components.holiday.*
homeassistant.components.homeassistant.*
homeassistant.components.homeassistant.exposed_entities
homeassistant.components.homeassistant.triggers.event
homeassistant.components.homeassistant_alerts.*
homeassistant.components.homeassistant_green.*
homeassistant.components.homeassistant_hardware.*
@@ -229,7 +170,6 @@ homeassistant.components.homekit_controller.utils
homeassistant.components.homewizard.*
homeassistant.components.http.*
homeassistant.components.huawei_lte.*
homeassistant.components.humidifier.*
homeassistant.components.hydrawise.*
homeassistant.components.hyperion.*
homeassistant.components.ibeacon.*
@@ -242,9 +182,6 @@ homeassistant.components.input_button.*
homeassistant.components.input_select.*
homeassistant.components.input_text.*
homeassistant.components.integration.*
homeassistant.components.intent.*
homeassistant.components.intent_script.*
homeassistant.components.ios.*
homeassistant.components.ipp.*
homeassistant.components.iqvia.*
homeassistant.components.islamic_prayer_times.*
@@ -257,13 +194,11 @@ homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.*
homeassistant.components.lamarzocco.*
homeassistant.components.lametric.*
homeassistant.components.laundrify.*
homeassistant.components.lawn_mower.*
homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
@@ -279,28 +214,22 @@ homeassistant.components.london_underground.*
homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.mailbox.*
homeassistant.components.map.*
homeassistant.components.mastodon.*
homeassistant.components.matrix.*
homeassistant.components.matter.*
homeassistant.components.media_extractor.*
homeassistant.components.media_player.*
homeassistant.components.media_source.*
homeassistant.components.met_eireann.*
homeassistant.components.metoffice.*
homeassistant.components.mikrotik.*
homeassistant.components.min_max.*
homeassistant.components.minecraft_server.*
homeassistant.components.mjpeg.*
homeassistant.components.modbus.*
homeassistant.components.modem_callerid.*
homeassistant.components.moon.*
homeassistant.components.mopeka.*
homeassistant.components.motionmount.*
homeassistant.components.mqtt.*
homeassistant.components.my.*
homeassistant.components.mysensors.*
homeassistant.components.myuplink.*
homeassistant.components.nam.*
homeassistant.components.nanoleaf.*
homeassistant.components.neato.*
@@ -309,24 +238,20 @@ homeassistant.components.netatmo.*
homeassistant.components.network.*
homeassistant.components.nextdns.*
homeassistant.components.nfandroidtv.*
homeassistant.components.nightscout.*
homeassistant.components.nissan_leaf.*
homeassistant.components.no_ip.*
homeassistant.components.notify.*
homeassistant.components.notion.*
homeassistant.components.number.*
homeassistant.components.nut.*
homeassistant.components.onboarding.*
homeassistant.components.oncue.*
homeassistant.components.onewire.*
homeassistant.components.open_meteo.*
homeassistant.components.openexchangerates.*
homeassistant.components.opensky.*
homeassistant.components.openuv.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.overkiz.*
homeassistant.components.p1_monitor.*
homeassistant.components.peco.*
homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.*
@@ -335,17 +260,13 @@ homeassistant.components.plugwise.*
homeassistant.components.poolsense.*
homeassistant.components.powerwall.*
homeassistant.components.private_ble_device.*
homeassistant.components.prometheus.*
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.rabbitair.*
homeassistant.components.radarr.*
homeassistant.components.rainforest_raven.*
homeassistant.components.rainmachine.*
homeassistant.components.raspberry_pi.*
homeassistant.components.rdw.*
@@ -355,13 +276,11 @@ homeassistant.components.remote.*
homeassistant.components.renault.*
homeassistant.components.repairs.*
homeassistant.components.rest.*
homeassistant.components.rest_command.*
homeassistant.components.rfxtrx.*
homeassistant.components.rhasspy.*
homeassistant.components.ridwell.*
homeassistant.components.rituals_perfume_genie.*
homeassistant.components.roku.*
homeassistant.components.romy.*
homeassistant.components.rpi_power.*
homeassistant.components.rss_feed_template.*
homeassistant.components.rtsp_to_webrtc.*
@@ -371,7 +290,6 @@ homeassistant.components.samsungtv.*
homeassistant.components.scene.*
homeassistant.components.schedule.*
homeassistant.components.scrape.*
homeassistant.components.search.*
homeassistant.components.select.*
homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.*
@@ -379,10 +297,8 @@ homeassistant.components.sensor.*
homeassistant.components.senz.*
homeassistant.components.sfr_box.*
homeassistant.components.shelly.*
homeassistant.components.shopping_list.*
homeassistant.components.simplepush.*
homeassistant.components.simplisafe.*
homeassistant.components.siren.*
homeassistant.components.skybell.*
homeassistant.components.slack.*
homeassistant.components.sleepiq.*
@@ -397,9 +313,6 @@ homeassistant.components.statistics.*
homeassistant.components.steamist.*
homeassistant.components.stookalert.*
homeassistant.components.stream.*
homeassistant.components.streamlabswater.*
homeassistant.components.stt.*
homeassistant.components.suez_water.*
homeassistant.components.sun.*
homeassistant.components.surepetcare.*
homeassistant.components.switch.*
@@ -407,31 +320,20 @@ homeassistant.components.switchbee.*
homeassistant.components.switchbot_cloud.*
homeassistant.components.switcher_kis.*
homeassistant.components.synology_dsm.*
homeassistant.components.system_health.*
homeassistant.components.system_log.*
homeassistant.components.systemmonitor.*
homeassistant.components.tag.*
homeassistant.components.tailscale.*
homeassistant.components.tailwind.*
homeassistant.components.tami4.*
homeassistant.components.tautulli.*
homeassistant.components.tcp.*
homeassistant.components.technove.*
homeassistant.components.tedee.*
homeassistant.components.text.*
homeassistant.components.threshold.*
homeassistant.components.tibber.*
homeassistant.components.tile.*
homeassistant.components.tilt_ble.*
homeassistant.components.time.*
homeassistant.components.time_date.*
homeassistant.components.timer.*
homeassistant.components.tod.*
homeassistant.components.todo.*
homeassistant.components.tolo.*
homeassistant.components.tplink.*
homeassistant.components.tplink_omada.*
homeassistant.components.trace.*
homeassistant.components.tractive.*
homeassistant.components.tradfri.*
homeassistant.components.trafikverket_camera.*
@@ -451,17 +353,13 @@ 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.*
homeassistant.components.wake_word.*
homeassistant.components.wallbox.*
homeassistant.components.waqi.*
homeassistant.components.water_heater.*
homeassistant.components.watttime.*
homeassistant.components.weather.*
homeassistant.components.webhook.*
homeassistant.components.webostv.*
homeassistant.components.websocket_api.*
homeassistant.components.wemo.*
@@ -470,10 +368,8 @@ homeassistant.components.withings.*
homeassistant.components.wiz.*
homeassistant.components.wled.*
homeassistant.components.worldclock.*
homeassistant.components.xiaomi_ble.*
homeassistant.components.yale_smart_alarm.*
homeassistant.components.yalexs_ble.*
homeassistant.components.youtube.*
homeassistant.components.zeroconf.*
homeassistant.components.zodiac.*
homeassistant.components.zone.*

14
.vscode/tasks.json vendored
View File

@@ -157,20 +157,6 @@
"kind": "build",
"isDefault": true
}
},
{
"label": "Install integration requirements",
"detail": "Install all requirements of a given integration.",
"type": "shell",
"command": "${command:python.interpreterPath} -m script.install_integration_requirements ${input:integrationName}",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new"
}
}
],
"inputs": [

View File

@@ -45,14 +45,12 @@ build.json @home-assistant/supervisor
/tests/components/airnow/ @asymworks
/homeassistant/components/airq/ @Sibgatulin @dl2080
/tests/components/airq/ @Sibgatulin @dl2080
/homeassistant/components/airthings/ @danielhiversen @LaStrada
/tests/components/airthings/ @danielhiversen @LaStrada
/homeassistant/components/airthings/ @danielhiversen
/tests/components/airthings/ @danielhiversen
/homeassistant/components/airthings_ble/ @vincegio @LaStrada
/tests/components/airthings_ble/ @vincegio @LaStrada
/homeassistant/components/airtouch4/ @samsinnamon
/tests/components/airtouch4/ @samsinnamon
/homeassistant/components/airtouch5/ @danzel
/tests/components/airtouch5/ @danzel
/homeassistant/components/airvisual/ @bachya
/tests/components/airvisual/ @bachya
/homeassistant/components/airvisual_pro/ @bachya
@@ -78,8 +76,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/amcrest/ @flacjacket
/homeassistant/components/analytics/ @home-assistant/core @ludeeus
/tests/components/analytics/ @home-assistant/core @ludeeus
/homeassistant/components/analytics_insights/ @joostlek
/tests/components/analytics_insights/ @joostlek
/homeassistant/components/android_ip_webcam/ @engrbm87
/tests/components/android_ip_webcam/ @engrbm87
/homeassistant/components/androidtv/ @JeffLIrion @ollo69
@@ -90,8 +86,6 @@ build.json @home-assistant/supervisor
/tests/components/anova/ @Lash-L
/homeassistant/components/anthemav/ @hyralex
/tests/components/anthemav/ @hyralex
/homeassistant/components/aosmith/ @bdr99
/tests/components/aosmith/ @bdr99
/homeassistant/components/apache_kafka/ @bachya
/tests/components/apache_kafka/ @bachya
/homeassistant/components/apcupsd/ @yuxincs
@@ -149,8 +143,6 @@ build.json @home-assistant/supervisor
/tests/components/baf/ @bdraco @jfroy
/homeassistant/components/balboa/ @garbled1 @natekspencer
/tests/components/balboa/ @garbled1 @natekspencer
/homeassistant/components/bang_olufsen/ @mj23000
/tests/components/bang_olufsen/ @mj23000
/homeassistant/components/bayesian/ @HarvsG
/tests/components/bayesian/ @HarvsG
/homeassistant/components/beewi_smartclim/ @alemuro
@@ -161,8 +153,6 @@ build.json @home-assistant/supervisor
/tests/components/blebox/ @bbx-a @riokuu
/homeassistant/components/blink/ @fronzbot @mkmer
/tests/components/blink/ @fronzbot @mkmer
/homeassistant/components/blue_current/ @Floris272 @gleeuwen
/tests/components/blue_current/ @Floris272 @gleeuwen
/homeassistant/components/bluemaestro/ @bdraco
/tests/components/bluemaestro/ @bdraco
/homeassistant/components/blueprint/ @home-assistant/core
@@ -180,8 +170,6 @@ build.json @home-assistant/supervisor
/tests/components/bosch_shc/ @tschamm
/homeassistant/components/braviatv/ @bieniu @Drafteed
/tests/components/braviatv/ @bieniu @Drafteed
/homeassistant/components/bring/ @miaucl @tr4nt0r
/tests/components/bring/ @miaucl @tr4nt0r
/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
/homeassistant/components/brother/ @bieniu
@@ -205,8 +193,6 @@ build.json @home-assistant/supervisor
/tests/components/camera/ @home-assistant/core
/homeassistant/components/cast/ @emontnemery
/tests/components/cast/ @emontnemery
/homeassistant/components/ccm15/ @ocalvo
/tests/components/ccm15/ @ocalvo
/homeassistant/components/cert_expiry/ @jjlawren
/tests/components/cert_expiry/ @jjlawren
/homeassistant/components/circuit/ @braam
@@ -219,8 +205,8 @@ build.json @home-assistant/supervisor
/tests/components/cloud/ @home-assistant/cloud
/homeassistant/components/cloudflare/ @ludeeus @ctalkington
/tests/components/cloudflare/ @ludeeus @ctalkington
/homeassistant/components/co2signal/ @jpbede @VIKTORVAV99
/tests/components/co2signal/ @jpbede @VIKTORVAV99
/homeassistant/components/co2signal/ @jpbede
/tests/components/co2signal/ @jpbede
/homeassistant/components/coinbase/ @tombrien
/tests/components/coinbase/ @tombrien
/homeassistant/components/color_extractor/ @GenericStudent
@@ -273,8 +259,6 @@ build.json @home-assistant/supervisor
/tests/components/denonavr/ @ol-iver @starkillerOG
/homeassistant/components/derivative/ @afaucogney
/tests/components/derivative/ @afaucogney
/homeassistant/components/devialet/ @fwestenberg
/tests/components/devialet/ @fwestenberg
/homeassistant/components/device_automation/ @home-assistant/core
/tests/components/device_automation/ @home-assistant/core
/homeassistant/components/device_tracker/ @home-assistant/core
@@ -309,8 +293,6 @@ build.json @home-assistant/supervisor
/tests/components/dormakaba_dkey/ @emontnemery
/homeassistant/components/dremel_3d_printer/ @tkdrob
/tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @depl0y @glodenox
@@ -325,12 +307,13 @@ build.json @home-assistant/supervisor
/tests/components/eafm/ @Jc2k
/homeassistant/components/easyenergy/ @klaasnicolaas
/tests/components/easyenergy/ @klaasnicolaas
/homeassistant/components/ecobee/ @marcolivierarsenault
/tests/components/ecobee/ @marcolivierarsenault
/homeassistant/components/ecoforest/ @pjanuario
/tests/components/ecoforest/ @pjanuario
/homeassistant/components/econet/ @w1ll1am23
/tests/components/econet/ @w1ll1am23
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus
/homeassistant/components/ecovacs/ @OverloadUT @mib1185
/homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli
/homeassistant/components/efergy/ @tkdrob
@@ -347,8 +330,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/elmax/ @albertogeniola
/tests/components/elmax/ @albertogeniola
/homeassistant/components/elv/ @majuss
/homeassistant/components/elvia/ @ludeeus
/tests/components/elvia/ @ludeeus
/homeassistant/components/emby/ @mezz64
/homeassistant/components/emoncms/ @borpin
/homeassistant/components/emonitor/ @bdraco
@@ -361,7 +342,7 @@ build.json @home-assistant/supervisor
/tests/components/energy/ @home-assistant/core
/homeassistant/components/energyzero/ @klaasnicolaas
/tests/components/energyzero/ @klaasnicolaas
/homeassistant/components/enigma2/ @autinerd
/homeassistant/components/enigma2/ @fbradyirl
/homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
@@ -370,8 +351,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50
/homeassistant/components/epion/ @lhgravendeel
/tests/components/epion/ @lhgravendeel
/homeassistant/components/epson/ @pszafer
/tests/components/epson/ @pszafer
/homeassistant/components/epsonworkforce/ @ThaStealth
@@ -414,8 +393,6 @@ build.json @home-assistant/supervisor
/tests/components/fivem/ @Sander0542
/homeassistant/components/fjaraskupan/ @elupus
/tests/components/fjaraskupan/ @elupus
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
/tests/components/flexit_bacnet/ @lellky @piotrbulinski
/homeassistant/components/flick_electric/ @ZephireNZ
/tests/components/flick_electric/ @ZephireNZ
/homeassistant/components/flipr/ @cnico
@@ -431,8 +408,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/forked_daapd/ @uvjustin
/tests/components/forked_daapd/ @uvjustin
/homeassistant/components/fortios/ @kimfrellsen
/homeassistant/components/foscam/ @skgsergio @krmarien
/tests/components/foscam/ @skgsergio @krmarien
/homeassistant/components/foscam/ @skgsergio
/tests/components/foscam/ @skgsergio
/homeassistant/components/freebox/ @hacf-fr @Quentame
/tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415
@@ -505,10 +482,7 @@ build.json @home-assistant/supervisor
/tests/components/google_travel_time/ @eifinger
/homeassistant/components/govee_ble/ @bdraco @PierreAronnax
/tests/components/govee_ble/ @bdraco @PierreAronnax
/homeassistant/components/govee_light_local/ @Galorhallen
/tests/components/govee_light_local/ @Galorhallen
/homeassistant/components/gpsd/ @fabaff @jrieger
/tests/components/gpsd/ @fabaff @jrieger
/homeassistant/components/gpsd/ @fabaff
/homeassistant/components/gree/ @cmroche
/tests/components/gree/ @cmroche
/homeassistant/components/greeneye_monitor/ @jkeljo
@@ -542,14 +516,12 @@ build.json @home-assistant/supervisor
/tests/components/history/ @home-assistant/core
/homeassistant/components/hive/ @Rendili @KJonline
/tests/components/hive/ @Rendili @KJonline
/homeassistant/components/hko/ @MisterCommand
/tests/components/hko/ @MisterCommand
/homeassistant/components/hlk_sw16/ @jameshilliard
/tests/components/hlk_sw16/ @jameshilliard
/homeassistant/components/holiday/ @jrieger @gjohansson-ST
/tests/components/holiday/ @jrieger @gjohansson-ST
/homeassistant/components/home_connect/ @DavidMStraub
/tests/components/home_connect/ @DavidMStraub
/homeassistant/components/home_plus_control/ @chemaaa
/tests/components/home_plus_control/ @chemaaa
/homeassistant/components/homeassistant/ @home-assistant/core
/tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
@@ -584,8 +556,6 @@ build.json @home-assistant/supervisor
/tests/components/humidifier/ @home-assistant/core @Shulyaka
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/huum/ @frwickst
/tests/components/huum/ @frwickst
/homeassistant/components/hvv_departures/ @vigonotion
/tests/components/hvv_departures/ @vigonotion
/homeassistant/components/hydrawise/ @dknowles2 @ptcryan
@@ -669,8 +639,8 @@ build.json @home-assistant/supervisor
/tests/components/juicenet/ @jesserockz
/homeassistant/components/justnimbus/ @kvanzuijlen
/tests/components/justnimbus/ @kvanzuijlen
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
/tests/components/jvc_projector/ @SteveEasley @msavazzi
/homeassistant/components/jvc_projector/ @SteveEasley
/tests/components/jvc_projector/ @SteveEasley
/homeassistant/components/kaiterra/ @Michsior14
/homeassistant/components/kaleidescape/ @SteveEasley
/tests/components/kaleidescape/ @SteveEasley
@@ -691,6 +661,8 @@ build.json @home-assistant/supervisor
/tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund
/tests/components/kodi/ @OnFreund
/homeassistant/components/komfovent/ @ProstoSanja
/tests/components/komfovent/ @ProstoSanja
/homeassistant/components/konnected/ @heythisisnate
/tests/components/konnected/ @heythisisnate
/homeassistant/components/kostal_plenticore/ @stegm
@@ -701,8 +673,6 @@ build.json @home-assistant/supervisor
/tests/components/kulersky/ @emlove
/homeassistant/components/lacrosse_view/ @IceBotYT
/tests/components/lacrosse_view/ @IceBotYT
/homeassistant/components/lamarzocco/ @zweckj
/tests/components/lamarzocco/ @zweckj
/homeassistant/components/lametric/ @robbiet480 @frenck @bachya
/tests/components/lametric/ @robbiet480 @frenck @bachya
/homeassistant/components/landisgyr_heat_meter/ @vpathuis
@@ -719,13 +689,13 @@ build.json @home-assistant/supervisor
/tests/components/lcn/ @alengwenus
/homeassistant/components/ld2410_ble/ @930913
/tests/components/ld2410_ble/ @930913
/homeassistant/components/leaone/ @bdraco
/tests/components/leaone/ @bdraco
/homeassistant/components/led_ble/ @bdraco
/tests/components/led_ble/ @bdraco
/homeassistant/components/lg_netcast/ @Drafteed
/homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob
/homeassistant/components/life360/ @pnbruckner
/tests/components/life360/ @pnbruckner
/homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core
/homeassistant/components/linear_garage_door/ @IceBotYT
@@ -762,10 +732,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/luci/ @mzdrale
/homeassistant/components/luftdaten/ @fabaff @frenck
/tests/components/luftdaten/ @fabaff @frenck
/homeassistant/components/lupusec/ @majuss @suaveolent
/tests/components/lupusec/ @majuss @suaveolent
/homeassistant/components/lutron/ @cdheiser @wilburCForce
/tests/components/lutron/ @cdheiser @wilburCForce
/homeassistant/components/lupusec/ @majuss
/homeassistant/components/lutron/ @cdheiser
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues
/tests/components/lutron_caseta/ @swails @bdraco @danaues
/homeassistant/components/lyric/ @timmo001
@@ -835,8 +803,6 @@ build.json @home-assistant/supervisor
/tests/components/motion_blinds/ @starkillerOG
/homeassistant/components/motioneye/ @dermotduffy
/tests/components/motioneye/ @dermotduffy
/homeassistant/components/motionmount/ @RJPoelstra
/tests/components/motionmount/ @RJPoelstra
/homeassistant/components/mqtt/ @emontnemery @jbouwh
/tests/components/mqtt/ @emontnemery @jbouwh
/homeassistant/components/msteams/ @peroyvind
@@ -850,8 +816,6 @@ build.json @home-assistant/supervisor
/tests/components/mysensors/ @MartinHjelmare @functionpointer
/homeassistant/components/mystrom/ @fabaff
/tests/components/mystrom/ @fabaff
/homeassistant/components/myuplink/ @pajzo
/tests/components/myuplink/ @pajzo
/homeassistant/components/nam/ @bieniu
/tests/components/nam/ @bieniu
/homeassistant/components/nanoleaf/ @milanmeu
@@ -869,7 +833,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG
/tests/components/netgear/ @hacf-fr @Quentame @starkillerOG
/homeassistant/components/netgear_lte/ @tkdrob
/tests/components/netgear_lte/ @tkdrob
/homeassistant/components/network/ @home-assistant/core
/tests/components/network/ @home-assistant/core
/homeassistant/components/nexia/ @bdraco
@@ -963,8 +926,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/oralb/ @bdraco @Lash-L
/tests/components/oralb/ @bdraco @Lash-L
/homeassistant/components/oru/ @bvlaicu
/homeassistant/components/osoenergy/ @osohotwateriot
/tests/components/osoenergy/ @osohotwateriot
/homeassistant/components/otbr/ @home-assistant/core
/tests/components/otbr/ @home-assistant/core
/homeassistant/components/ourgroceries/ @OnFreund
@@ -1024,8 +985,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
/homeassistant/components/prusalink/ @balloob @Skaronator
/tests/components/prusalink/ @balloob @Skaronator
/homeassistant/components/prusalink/ @balloob
/tests/components/prusalink/ @balloob
/homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45
/homeassistant/components/pure_energie/ @klaasnicolaas
@@ -1042,8 +1003,8 @@ build.json @home-assistant/supervisor
/tests/components/pvoutput/ @frenck
/homeassistant/components/pvpc_hourly_pricing/ @azogue
/tests/components/pvpc_hourly_pricing/ @azogue
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
/homeassistant/components/qbittorrent/ @geoffreylagaisse
/tests/components/qbittorrent/ @geoffreylagaisse
/homeassistant/components/qingping/ @bdraco @skgsergio
/tests/components/qingping/ @bdraco @skgsergio
/homeassistant/components/qld_bushfire/ @exxamalte
@@ -1056,10 +1017,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/qvr_pro/ @oblogic7
/homeassistant/components/qwikswitch/ @kellerza
/tests/components/qwikswitch/ @kellerza
/homeassistant/components/rabbitair/ @rabbit-air
/tests/components/rabbitair/ @rabbit-air
/homeassistant/components/rachio/ @bdraco @rfverbruggen
/tests/components/rachio/ @bdraco @rfverbruggen
/homeassistant/components/rachio/ @bdraco
/tests/components/rachio/ @bdraco
/homeassistant/components/radarr/ @tkdrob
/tests/components/radarr/ @tkdrob
/homeassistant/components/radio_browser/ @frenck
@@ -1071,8 +1030,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/raincloud/ @vanstinator
/homeassistant/components/rainforest_eagle/ @gtdiehl @jcalbert @hastarin
/tests/components/rainforest_eagle/ @gtdiehl @jcalbert @hastarin
/homeassistant/components/rainforest_raven/ @cottsay
/tests/components/rainforest_raven/ @cottsay
/homeassistant/components/rainmachine/ @bachya
/tests/components/rainmachine/ @bachya
/homeassistant/components/random/ @fabaff
@@ -1089,8 +1046,6 @@ build.json @home-assistant/supervisor
/tests/components/recorder/ @home-assistant/core
/homeassistant/components/recovery_mode/ @home-assistant/core
/tests/components/recovery_mode/ @home-assistant/core
/homeassistant/components/refoss/ @ashionky
/tests/components/refoss/ @ashionky
/homeassistant/components/rejseplanen/ @DarkFox
/homeassistant/components/remote/ @home-assistant/core
/tests/components/remote/ @home-assistant/core
@@ -1103,8 +1058,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/repairs/ @home-assistant/core
/tests/components/repairs/ @home-assistant/core
/homeassistant/components/repetier/ @ShadowBr0ther
/homeassistant/components/rest_command/ @jpbede
/tests/components/rest_command/ @jpbede
/homeassistant/components/rflink/ @javicalle
/tests/components/rflink/ @javicalle
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
@@ -1125,8 +1078,6 @@ build.json @home-assistant/supervisor
/tests/components/roborock/ @humbertogontijo @Lash-L
/homeassistant/components/roku/ @ctalkington
/tests/components/roku/ @ctalkington
/homeassistant/components/romy/ @xeniter
/tests/components/romy/ @xeniter
/homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Xitee1
/homeassistant/components/roon/ @pavoni
@@ -1292,17 +1243,13 @@ build.json @home-assistant/supervisor
/homeassistant/components/subaru/ @G-Two
/tests/components/subaru/ @G-Two
/homeassistant/components/suez_water/ @ooii
/tests/components/suez_water/ @ooii
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/sunweg/ @rokam
/tests/components/sunweg/ @rokam
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen
/homeassistant/components/swiss_hydrological_data/ @fabaff
/homeassistant/components/swiss_public_transport/ @fabaff @miaucl
/tests/components/swiss_public_transport/ @fabaff @miaucl
/homeassistant/components/swiss_public_transport/ @fabaff
/homeassistant/components/switch/ @home-assistant/core
/tests/components/switch/ @home-assistant/core
/homeassistant/components/switch_as_x/ @home-assistant/core
@@ -1325,46 +1272,34 @@ build.json @home-assistant/supervisor
/homeassistant/components/synology_srm/ @aerialls
/homeassistant/components/system_bridge/ @timmo001
/tests/components/system_bridge/ @timmo001
/homeassistant/components/systemmonitor/ @gjohansson-ST
/tests/components/systemmonitor/ @gjohansson-ST
/homeassistant/components/tado/ @chiefdragon @erwindouna
/tests/components/tado/ @chiefdragon @erwindouna
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
/tests/components/tado/ @michaelarnauts @chiefdragon
/homeassistant/components/tag/ @balloob @dmulcahey
/tests/components/tag/ @balloob @dmulcahey
/homeassistant/components/tailscale/ @frenck
/tests/components/tailscale/ @frenck
/homeassistant/components/tailwind/ @frenck
/tests/components/tailwind/ @frenck
/homeassistant/components/tami4/ @Guy293
/tests/components/tami4/ @Guy293
/homeassistant/components/tankerkoenig/ @guillempages @mib1185 @jpbede
/tests/components/tankerkoenig/ @guillempages @mib1185 @jpbede
/homeassistant/components/tankerkoenig/ @guillempages @mib1185
/tests/components/tankerkoenig/ @guillempages @mib1185
/homeassistant/components/tapsaff/ @bazwilliams
/homeassistant/components/tasmota/ @emontnemery
/tests/components/tasmota/ @emontnemery
/homeassistant/components/tautulli/ @ludeeus @tkdrob
/tests/components/tautulli/ @ludeeus @tkdrob
/homeassistant/components/technove/ @Moustachauve
/tests/components/technove/ @Moustachauve
/homeassistant/components/tedee/ @patrickhilker @zweckj
/tests/components/tedee/ @patrickhilker @zweckj
/homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
/homeassistant/components/tesla_wall_connector/ @einarhauks
/tests/components/tesla_wall_connector/ @einarhauks
/homeassistant/components/teslemetry/ @Bre77
/tests/components/teslemetry/ @Bre77
/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
/homeassistant/components/thermobeacon/ @bdraco
/tests/components/thermobeacon/ @bdraco
/homeassistant/components/thermopro/ @bdraco @h3ss
/tests/components/thermopro/ @bdraco @h3ss
/homeassistant/components/thermopro/ @bdraco
/tests/components/thermopro/ @bdraco
/homeassistant/components/thethingsnetwork/ @fabaff
/homeassistant/components/thread/ @home-assistant/core
/tests/components/thread/ @home-assistant/core
@@ -1389,14 +1324,12 @@ build.json @home-assistant/supervisor
/tests/components/tomorrowio/ @raman325 @lymanepp
/homeassistant/components/totalconnect/ @austinmroczek
/tests/components/totalconnect/ @austinmroczek
/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco @sdb9696
/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco
/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco
/homeassistant/components/tplink_omada/ @MarkGodwin
/tests/components/tplink_omada/ @MarkGodwin
/homeassistant/components/traccar/ @ludeeus
/tests/components/traccar/ @ludeeus
/homeassistant/components/traccar_server/ @ludeeus
/tests/components/traccar_server/ @ludeeus
/homeassistant/components/trace/ @home-assistant/core
/tests/components/trace/ @home-assistant/core
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
@@ -1427,7 +1360,6 @@ build.json @home-assistant/supervisor
/tests/components/ukraine_alarm/ @PaulAnnekov
/homeassistant/components/unifi/ @Kane610
/tests/components/unifi/ @Kane610
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
/tests/components/unifiprotect/ @AngellusMortis @bdraco
@@ -1456,8 +1388,6 @@ build.json @home-assistant/supervisor
/tests/components/vacuum/ @home-assistant/core
/homeassistant/components/vallox/ @andre-richter @slovdahl @viiru-
/tests/components/vallox/ @andre-richter @slovdahl @viiru-
/homeassistant/components/valve/ @home-assistant/core
/tests/components/valve/ @home-assistant/core
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342
@@ -1466,8 +1396,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/versasense/ @imstevenxyz
/homeassistant/components/version/ @ludeeus
/tests/components/version/ @ludeeus
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey
/homeassistant/components/vicare/ @CFenner
/tests/components/vicare/ @CFenner
/homeassistant/components/vilfo/ @ManneW
@@ -1586,7 +1516,7 @@ build.json @home-assistant/supervisor
/tests/components/zodiac/ @JulienTant
/homeassistant/components/zone/ @home-assistant/core
/tests/components/zone/ @home-assistant/core
/homeassistant/components/zoneminder/ @rohankapoorcom @nabbi
/homeassistant/components/zoneminder/ @rohankapoorcom
/homeassistant/components/zwave_js/ @home-assistant/z-wave
/tests/components/zwave_js/ @home-assistant/z-wave
/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS

View File

@@ -1,12 +1,9 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM
FROM ${BUILD_FROM}
# Synchronize with homeassistant/core.py:async_stop
ENV \
S6_SERVICES_GRACETIME=240000
S6_SERVICES_GRACETIME=220000
ARG QEMU_CPU
@@ -28,19 +25,12 @@ RUN \
&& if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \
pip3 install homeassistant/home_assistant_intents-*.whl; \
fi \
&& if [ "${BUILD_ARCH}" = "i386" ]; then \
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
linux32 pip3 install \
--only-binary=:all: \
-r homeassistant/requirements_all.txt; \
else \
&& \
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
pip3 install \
--only-binary=:all: \
-r homeassistant/requirements_all.txt; \
fi
-r homeassistant/requirements_all.txt
## Setup Home Assistant Core
COPY . homeassistant/

View File

@@ -16,7 +16,6 @@ RUN \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
# Additional library needed by some tests and accordingly by VScode Tests Discovery
bluez \
ffmpeg \
libudev-dev \
libavformat-dev \
libavcodec-dev \

View File

@@ -22,7 +22,7 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
:target: https://www.home-assistant.io/join-chat/
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-states.png
.. |screenshot-states| image:: https://raw.githubusercontent.com/home-assistant/core/master/docs/screenshots.png
:target: https://demo.home-assistant.io
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
:target: https://home-assistant.io/integrations/
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/docs/screenshot-integrations.png
:target: https://home-assistant.io/integrations/

View File

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

View File

@@ -4,27 +4,18 @@ from __future__ import annotations
import asyncio
from collections import OrderedDict
from collections.abc import Mapping
from datetime import datetime, timedelta
from functools import partial
from datetime import timedelta
import time
from typing import Any, cast
import jwt
from homeassistant import data_entry_flow
from homeassistant.core import (
CALLBACK_TYPE,
HassJob,
HassJobType,
HomeAssistant,
callback,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util
from . import auth_store, jwt_wrapper, models
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION
from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
@@ -56,7 +47,6 @@ async def auth_manager_from_config(
mfa modules exist in configs.
"""
store = auth_store.AuthStore(hass)
await store.async_load()
if provider_configs:
providers = await asyncio.gather(
*(
@@ -84,7 +74,6 @@ async def auth_manager_from_config(
module_hash[module.id] = module
manager = AuthManager(hass, store, provider_hash, module_hash)
manager.async_setup()
return manager
@@ -168,22 +157,7 @@ class AuthManager:
self._providers = providers
self._mfa_modules = mfa_modules
self.login_flow = AuthManagerFlowManager(hass, self)
self._revoke_callbacks: dict[str, set[CALLBACK_TYPE]] = {}
self._expire_callback: CALLBACK_TYPE | None = None
self._remove_expired_job = HassJob(
self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback
)
@callback
def async_setup(self) -> None:
"""Set up the auth manager."""
hass = self.hass
hass.async_add_shutdown_job(
HassJob(
self._async_cancel_expiration_schedule, job_type=HassJobType.Callback
)
)
self._async_track_next_refresh_token_expiration()
self._revoke_callbacks: dict[str, list[CALLBACK_TYPE]] = {}
@property
def auth_providers(self) -> list[AuthProvider]:
@@ -449,11 +423,6 @@ class AuthManager:
else:
token_type = models.TOKEN_TYPE_NORMAL
if token_type is models.TOKEN_TYPE_NORMAL:
expire_at = time.time() + REFRESH_TOKEN_EXPIRATION
else:
expire_at = None
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError(
"System generated users can only have system type refresh tokens"
@@ -485,81 +454,48 @@ class AuthManager:
client_icon,
token_type,
access_token_expiration,
expire_at,
credential,
)
@callback
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
async def async_get_refresh_token(
self, token_id: str
) -> models.RefreshToken | None:
"""Get refresh token by id."""
return self._store.async_get_refresh_token(token_id)
return await self._store.async_get_refresh_token(token_id)
@callback
def async_get_refresh_token_by_token(
async def async_get_refresh_token_by_token(
self, token: str
) -> models.RefreshToken | None:
"""Get refresh token by token."""
return self._store.async_get_refresh_token_by_token(token)
return await self._store.async_get_refresh_token_by_token(token)
@callback
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken
) -> None:
"""Delete a refresh token."""
self._store.async_remove_refresh_token(refresh_token)
await self._store.async_remove_refresh_token(refresh_token)
callbacks = self._revoke_callbacks.pop(refresh_token.id, ())
callbacks = self._revoke_callbacks.pop(refresh_token.id, [])
for revoke_callback in callbacks:
revoke_callback()
@callback
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
"""Remove expired refresh tokens."""
now = time.time()
for token in self._store.async_get_refresh_tokens():
if (expire_at := token.expire_at) is not None and expire_at <= now:
self.async_remove_refresh_token(token)
self._async_track_next_refresh_token_expiration()
@callback
def _async_track_next_refresh_token_expiration(self) -> None:
"""Initialise all token expiration scheduled tasks."""
next_expiration = time.time() + REFRESH_TOKEN_EXPIRATION
for token in self._store.async_get_refresh_tokens():
if (
expire_at := token.expire_at
) is not None and expire_at < next_expiration:
next_expiration = expire_at
self._expire_callback = async_track_point_in_utc_time(
self.hass,
self._remove_expired_job,
dt_util.utc_from_timestamp(next_expiration),
)
@callback
def _async_cancel_expiration_schedule(self) -> None:
"""Cancel tracking of expired refresh tokens."""
if self._expire_callback:
self._expire_callback()
self._expire_callback = None
@callback
def _async_unregister(
self, callbacks: set[CALLBACK_TYPE], callback_: CALLBACK_TYPE
) -> None:
"""Unregister a callback."""
callbacks.remove(callback_)
@callback
def async_register_revoke_token_callback(
self, refresh_token_id: str, revoke_callback: CALLBACK_TYPE
) -> CALLBACK_TYPE:
"""Register a callback to be called when the refresh token id is revoked."""
if refresh_token_id not in self._revoke_callbacks:
self._revoke_callbacks[refresh_token_id] = set()
self._revoke_callbacks[refresh_token_id] = []
callbacks = self._revoke_callbacks[refresh_token_id]
callbacks.add(revoke_callback)
return partial(self._async_unregister, callbacks, revoke_callback)
callbacks.append(revoke_callback)
@callback
def unregister() -> None:
if revoke_callback in callbacks:
callbacks.remove(revoke_callback)
return unregister
@callback
def async_create_access_token(
@@ -616,15 +552,16 @@ class AuthManager:
if provider := self._async_resolve_provider(refresh_token):
provider.async_validate_refresh_token(refresh_token, remote_ip)
@callback
def async_validate_access_token(self, token: str) -> models.RefreshToken | None:
async def async_validate_access_token(
self, token: str
) -> models.RefreshToken | None:
"""Return refresh token if an access token is valid."""
try:
unverif_claims = jwt_wrapper.unverified_hs256_token_decode(token)
except jwt.InvalidTokenError:
return None
refresh_token = self.async_get_refresh_token(
refresh_token = await self.async_get_refresh_token(
cast(str, unverif_claims.get("iss"))
)

View File

@@ -1,9 +1,10 @@
"""Storage for auth models."""
from __future__ import annotations
import asyncio
from collections import OrderedDict
from datetime import timedelta
import hmac
import itertools
from logging import getLogger
from typing import Any
@@ -18,7 +19,6 @@ from .const import (
GROUP_ID_ADMIN,
GROUP_ID_READ_ONLY,
GROUP_ID_USER,
REFRESH_TOKEN_EXPIRATION,
)
from .permissions import system_policies
from .permissions.models import PermissionLookup
@@ -43,28 +43,44 @@ class AuthStore:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the auth store."""
self.hass = hass
self._loaded = False
self._users: dict[str, models.User] = None # type: ignore[assignment]
self._groups: dict[str, models.Group] = None # type: ignore[assignment]
self._perm_lookup: PermissionLookup = None # type: ignore[assignment]
self._users: dict[str, models.User] | None = None
self._groups: dict[str, models.Group] | None = None
self._perm_lookup: PermissionLookup | None = None
self._store = Store[dict[str, list[dict[str, Any]]]](
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
)
self._lock = asyncio.Lock()
async def async_get_groups(self) -> list[models.Group]:
"""Retrieve all users."""
if self._groups is None:
await self._async_load()
assert self._groups is not None
return list(self._groups.values())
async def async_get_group(self, group_id: str) -> models.Group | None:
"""Retrieve all users."""
if self._groups is None:
await self._async_load()
assert self._groups is not None
return self._groups.get(group_id)
async def async_get_users(self) -> list[models.User]:
"""Retrieve all users."""
if self._users is None:
await self._async_load()
assert self._users is not None
return list(self._users.values())
async def async_get_user(self, user_id: str) -> models.User | None:
"""Retrieve a user by id."""
if self._users is None:
await self._async_load()
assert self._users is not None
return self._users.get(user_id)
async def async_create_user(
@@ -78,6 +94,12 @@ class AuthStore:
local_only: bool | None = None,
) -> models.User:
"""Create a new user."""
if self._users is None:
await self._async_load()
assert self._users is not None
assert self._groups is not None
groups = []
for group_id in group_ids or []:
if (group := self._groups.get(group_id)) is None:
@@ -123,6 +145,10 @@ class AuthStore:
async def async_remove_user(self, user: models.User) -> None:
"""Remove a user."""
if self._users is None:
await self._async_load()
assert self._users is not None
self._users.pop(user.id)
self._async_schedule_save()
@@ -135,6 +161,8 @@ class AuthStore:
local_only: bool | None = None,
) -> None:
"""Update a user."""
assert self._groups is not None
if group_ids is not None:
groups = []
for grid in group_ids:
@@ -143,6 +171,7 @@ class AuthStore:
groups.append(group)
user.groups = groups
user.invalidate_permission_cache()
for attr_name, value in (
("name", name),
@@ -166,6 +195,10 @@ class AuthStore:
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values():
found = None
@@ -188,7 +221,6 @@ class AuthStore:
client_icon: str | None = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
expire_at: float | None = None,
credential: models.Credentials | None = None,
) -> models.RefreshToken:
"""Create a new token for a user."""
@@ -197,7 +229,6 @@ class AuthStore:
"client_id": client_id,
"token_type": token_type,
"access_token_expiration": access_token_expiration,
"expire_at": expire_at,
"credential": credential,
}
if client_name:
@@ -211,17 +242,27 @@ class AuthStore:
self._async_schedule_save()
return refresh_token
@callback
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken
) -> None:
"""Remove a refresh token."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values():
if user.refresh_tokens.pop(refresh_token.id, None):
self._async_schedule_save()
break
@callback
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
async def async_get_refresh_token(
self, token_id: str
) -> models.RefreshToken | None:
"""Get refresh token by id."""
if self._users is None:
await self._async_load()
assert self._users is not None
for user in self._users.values():
refresh_token = user.refresh_tokens.get(token_id)
if refresh_token is not None:
@@ -229,11 +270,14 @@ class AuthStore:
return None
@callback
def async_get_refresh_token_by_token(
async def async_get_refresh_token_by_token(
self, token: str
) -> models.RefreshToken | None:
"""Get refresh token by token."""
if self._users is None:
await self._async_load()
assert self._users is not None
found = None
for user in self._users.values():
@@ -243,15 +287,6 @@ class AuthStore:
return found
@callback
def async_get_refresh_tokens(self) -> list[models.RefreshToken]:
"""Get all refresh tokens."""
return list(
itertools.chain.from_iterable(
user.refresh_tokens.values() for user in self._users.values()
)
)
@callback
def async_log_refresh_token_usage(
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
@@ -259,34 +294,35 @@ class AuthStore:
"""Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow()
refresh_token.last_used_ip = remote_ip
if refresh_token.expire_at:
refresh_token.expire_at = (
refresh_token.last_used_at.timestamp() + REFRESH_TOKEN_EXPIRATION
)
self._async_schedule_save()
async def async_load(self) -> None: # noqa: C901
async def _async_load(self) -> None:
"""Load the users."""
if self._loaded:
raise RuntimeError("Auth storage is already loaded")
self._loaded = True
async with self._lock:
if self._users is not None:
return
await self._async_load_task()
async def _async_load_task(self) -> None:
"""Load the users."""
dev_reg = dr.async_get(self.hass)
ent_reg = er.async_get(self.hass)
data = await self._store.async_load()
perm_lookup = PermissionLookup(ent_reg, dev_reg)
self._perm_lookup = perm_lookup
# Make sure that we're not overriding data if 2 loads happened at the
# same time
if self._users is not None:
return
now_ts = dt_util.utcnow().timestamp()
self._perm_lookup = perm_lookup = PermissionLookup(ent_reg, dev_reg)
if data is None or not isinstance(data, dict):
self._set_defaults()
return
users: dict[str, models.User] = {}
groups: dict[str, models.Group] = {}
credentials: dict[str, models.Credentials] = {}
users: dict[str, models.User] = OrderedDict()
groups: dict[str, models.Group] = OrderedDict()
credentials: dict[str, models.Credentials] = OrderedDict()
# Soft-migrating data as we load. We are going to make sure we have a
# read only group and an admin group. There are two states that we can
@@ -433,14 +469,6 @@ class AuthStore:
else:
last_used_at = None
if (
expire_at := rt_dict.get("expire_at")
) is None and token_type == models.TOKEN_TYPE_NORMAL:
if last_used_at:
expire_at = last_used_at.timestamp() + REFRESH_TOKEN_EXPIRATION
else:
expire_at = now_ts + REFRESH_TOKEN_EXPIRATION
token = models.RefreshToken(
id=rt_dict["id"],
user=users[rt_dict["user_id"]],
@@ -457,7 +485,6 @@ class AuthStore:
jwt_key=rt_dict["jwt_key"],
last_used_at=last_used_at,
last_used_ip=rt_dict.get("last_used_ip"),
expire_at=expire_at,
version=rt_dict.get("version"),
)
if "credential_id" in rt_dict:
@@ -467,16 +494,20 @@ class AuthStore:
self._groups = groups
self._users = users
self._async_schedule_save()
@callback
def _async_schedule_save(self) -> None:
"""Save users."""
if self._users is None:
return
self._store.async_delay_save(self._data_to_save, 1)
@callback
def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
"""Return the data to store."""
assert self._users is not None
assert self._groups is not None
users = [
{
"id": user.id,
@@ -533,7 +564,6 @@ class AuthStore:
if refresh_token.last_used_at
else None,
"last_used_ip": refresh_token.last_used_ip,
"expire_at": refresh_token.expire_at,
"credential_id": refresh_token.credential.id
if refresh_token.credential
else None,
@@ -552,9 +582,9 @@ class AuthStore:
def _set_defaults(self) -> None:
"""Set default values for auth store."""
self._users = {}
self._users = OrderedDict()
groups: dict[str, models.Group] = {}
groups: dict[str, models.Group] = OrderedDict()
admin_group = _system_admin_group()
groups[admin_group.id] = admin_group
user_group = _system_user_group()

View File

@@ -3,7 +3,6 @@ from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
REFRESH_TOKEN_EXPIRATION = timedelta(days=90).total_seconds()
GROUP_ID_ADMIN = "system-admin"
GROUP_ID_USER = "system-users"

View File

@@ -152,7 +152,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
"""Return list of notify services."""
unordered_services = set()
for service in self.hass.services.async_services_for_domain("notify"):
for service in self.hass.services.async_services().get("notify", {}):
if service not in self._exclude:
unordered_services.add(service)

View File

@@ -3,12 +3,10 @@ from __future__ import annotations
from datetime import datetime, timedelta
import secrets
from typing import TYPE_CHECKING, Any, NamedTuple
from typing import NamedTuple
import uuid
import attr
from attr import Attribute
from attr.setters import validate
from homeassistant.const import __version__
from homeassistant.util import dt as dt_util
@@ -16,12 +14,6 @@ from homeassistant.util import dt as dt_util
from . import permissions as perm_mdl
from .const import GROUP_ID_ADMIN
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
TOKEN_TYPE_NORMAL = "normal"
TOKEN_TYPE_SYSTEM = "system"
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
@@ -37,27 +29,19 @@ class Group:
system_generated: bool = attr.ib(default=False)
def _handle_permissions_change(self: User, user_attr: Attribute, new: Any) -> Any:
"""Handle a change to a permissions."""
self.invalidate_cache()
return validate(self, user_attr, new)
@attr.s(slots=False)
@attr.s(slots=True)
class User:
"""A user."""
name: str | None = attr.ib()
perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False)
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
is_owner: bool = attr.ib(default=False, on_setattr=_handle_permissions_change)
is_active: bool = attr.ib(default=False, on_setattr=_handle_permissions_change)
is_owner: bool = attr.ib(default=False)
is_active: bool = attr.ib(default=False)
system_generated: bool = attr.ib(default=False)
local_only: bool = attr.ib(default=False)
groups: list[Group] = attr.ib(
factory=list, eq=False, order=False, on_setattr=_handle_permissions_change
)
groups: list[Group] = attr.ib(factory=list, eq=False, order=False)
# List of credentials of a user.
credentials: list[Credentials] = attr.ib(factory=list, eq=False, order=False)
@@ -67,31 +51,40 @@ class User:
factory=dict, eq=False, order=False
)
@cached_property
_permissions: perm_mdl.PolicyPermissions | None = attr.ib(
init=False,
eq=False,
order=False,
default=None,
)
@property
def permissions(self) -> perm_mdl.AbstractPermissions:
"""Return permissions object for user."""
if self.is_owner:
return perm_mdl.OwnerPermissions
return perm_mdl.PolicyPermissions(
if self._permissions is not None:
return self._permissions
self._permissions = perm_mdl.PolicyPermissions(
perm_mdl.merge_policies([group.policy for group in self.groups]),
self.perm_lookup,
)
@cached_property
return self._permissions
@property
def is_admin(self) -> bool:
"""Return if user is part of the admin group."""
return self.is_owner or (
self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups)
)
if self.is_owner:
return True
def invalidate_cache(self) -> None:
"""Invalidate permission and is_admin cache."""
for attr_to_invalidate in ("permissions", "is_admin"):
# try is must more efficient than suppress
try: # noqa: SIM105
delattr(self, attr_to_invalidate)
except AttributeError:
pass
return self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups)
def invalidate_permission_cache(self) -> None:
"""Invalidate permission cache."""
self._permissions = None
@attr.s(slots=True)
@@ -117,8 +110,6 @@ class RefreshToken:
last_used_at: datetime | None = attr.ib(default=None)
last_used_ip: str | None = attr.ib(default=None)
expire_at: float | None = attr.ib(default=None)
credential: Credentials | None = attr.ib(default=None)
version: str | None = attr.ib(default=__version__)

View File

@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Any
import voluptuous as vol
import yarl
from . import config as conf_util, config_entries, core, loader, requirements
from . import config as conf_util, config_entries, core, loader
from .components import http
from .const import (
FORMAT_DATETIME,
@@ -27,7 +27,6 @@ from .const import (
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
config_validation as cv,
device_registry,
entity,
entity_registry,
@@ -39,6 +38,7 @@ from .helpers import (
from .helpers.dispatcher import async_dispatcher_send
from .helpers.typing import ConfigType
from .setup import (
DATA_SETUP,
DATA_SETUP_STARTED,
DATA_SETUP_TIME,
async_notify_setup_error,
@@ -105,52 +105,6 @@ STAGE_1_INTEGRATIONS = {
# Ensure supervisor is available
"hassio",
}
DEFAULT_INTEGRATIONS = {
# These integrations are set up unless recovery mode is activated.
#
# Integrations providing core functionality:
"application_credentials",
"frontend",
"hardware",
"logger",
"network",
"system_health",
#
# Key-feature:
"automation",
"person",
"scene",
"script",
"tag",
"zone",
#
# Built-in helpers:
"counter",
"input_boolean",
"input_button",
"input_datetime",
"input_number",
"input_select",
"input_text",
"schedule",
"timer",
}
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
# These integrations are set up if recovery mode is activated.
"frontend",
}
DEFAULT_INTEGRATIONS_SUPERVISOR = {
# These integrations are set up if using the Supervisor
"hassio",
}
DEFAULT_INTEGRATIONS_NON_SUPERVISOR = {
# These integrations are set up if not using the Supervisor
"backup",
}
CRITICAL_INTEGRATIONS = {
# Recovery mode is activated if these integrations fail to set up
"frontend",
}
async def async_setup_hass(
@@ -210,11 +164,11 @@ async def async_setup_hass(
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
recovery_mode = True
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
_LOGGER.warning(
"Detected that %s did not load. Activating recovery mode",
",".join(CRITICAL_INTEGRATIONS),
)
elif (
"frontend" in hass.data.get(DATA_SETUP, {})
and "frontend" not in hass.config.components
):
_LOGGER.warning("Detected that frontend did not load. Activating recovery mode")
# Ask integrations to shut down. It's messy but we can't
# do a clean stop without knowing what is broken
with contextlib.suppress(asyncio.TimeoutError):
@@ -274,7 +228,7 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
)
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
async def load_registries(hass: core.HomeAssistant) -> None:
"""Load the registries and cache the result of platform.uname().processor."""
if DATA_REGISTRIES_LOADED in hass.data:
return
@@ -301,7 +255,6 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
hass.async_add_executor_job(_cache_uname_processor),
template.async_load_custom_templates(hass),
restore_state.async_load(hass),
hass.config_entries.async_initialize(),
)
@@ -316,7 +269,8 @@ async def async_from_config_dict(
start = monotonic()
hass.config_entries = config_entries.ConfigEntries(hass, config)
await async_load_base_functionality(hass)
await hass.config_entries.async_initialize()
await load_registries(hass)
# Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
@@ -519,22 +473,15 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
"""Get domains of components to set up."""
# Filter out the repeating and common config section [homeassistant]
domains = {
domain for key in config if (domain := cv.domain_key(key)) != core.DOMAIN
}
domains = {key.partition(" ")[0] for key in config if key != core.DOMAIN}
# Add config entry and default domains
# Add config entry domains
if not hass.config.recovery_mode:
domains.update(DEFAULT_INTEGRATIONS)
domains.update(hass.config_entries.async_domains())
else:
domains.update(DEFAULT_INTEGRATIONS_RECOVERY_MODE)
# Add domains depending on if the Supervisor is used or not
# Make sure the Hass.io component is loaded
if "SUPERVISOR" in os.environ:
domains.update(DEFAULT_INTEGRATIONS_SUPERVISOR)
else:
domains.update(DEFAULT_INTEGRATIONS_NON_SUPERVISOR)
domains.add("hassio")
return domains
@@ -577,13 +524,11 @@ async def async_setup_multi_components(
config: dict[str, Any],
) -> None:
"""Set up multiple domains. Log on failure."""
# Avoid creating tasks for domains that were setup in a previous stage
domains_not_yet_setup = domains - hass.config.components
futures = {
domain: hass.async_create_task(
async_setup_component(hass, domain, config), f"setup component {domain}"
)
for domain in domains_not_yet_setup
for domain in domains
}
results = await asyncio.gather(*futures.values(), return_exceptions=True)
for idx, domain in enumerate(futures):
@@ -607,8 +552,6 @@ async def _async_set_up_integrations(
domains_to_setup = _get_domains(hass, config)
needed_requirements: set[str] = set()
# Resolve all dependencies so we know all integrations
# that will have to be loaded and start rightaway
integration_cache: dict[str, loader.Integration] = {}
@@ -624,25 +567,6 @@ async def _async_set_up_integrations(
).values()
if isinstance(int_or_exc, loader.Integration)
]
manifest_deps: set[str] = set()
for itg in integrations_to_process:
manifest_deps.update(itg.dependencies)
manifest_deps.update(itg.after_dependencies)
needed_requirements.update(itg.requirements)
if manifest_deps:
# If there are dependencies, try to preload all
# the integrations manifest at once and add them
# to the list of requirements we need to install
# so we can try to check if they are already installed
# in a single call below which avoids each integration
# having to wait for the lock to do it individually
deps = await loader.async_get_integrations(hass, manifest_deps)
for dependant_itg in deps.values():
if isinstance(dependant_itg, loader.Integration):
needed_requirements.update(dependant_itg.requirements)
resolve_dependencies_tasks = [
itg.resolve_dependencies()
for itg in integrations_to_process
@@ -664,14 +588,6 @@ async def _async_set_up_integrations(
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
# Optimistically check if requirements are already installed
# ahead of setting up the integrations so we can prime the cache
# We do not wait for this since its an optimization only
hass.async_create_background_task(
requirements.async_load_installed_versions(hass, needed_requirements),
"check installed requirements",
)
# Initialize recorder
if "recorder" in domains_to_setup:
recorder.async_initialize_recorder(hass)

View File

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

View File

@@ -1,5 +0,0 @@
{
"domain": "govee",
"name": "Govee",
"integrations": ["govee_ble", "govee_light_local"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "rainforest_automation",
"name": "Rainforest Automation",
"integrations": ["rainforest_eagle", "rainforest_raven"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "traccar",
"name": "Traccar",
"integrations": ["traccar", "traccar_server"]
}

View File

@@ -11,7 +11,6 @@ from __future__ import annotations
import logging
from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers.group import expand_entity_ids
_LOGGER = logging.getLogger(__name__)
@@ -22,7 +21,7 @@ def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool:
If there is no entity id given we will check all.
"""
if entity_id:
entity_ids = expand_entity_ids(hass, [entity_id])
entity_ids = hass.components.group.expand_entity_ids([entity_id])
else:
entity_ids = hass.states.entity_ids()

View File

@@ -63,12 +63,12 @@ AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.CAMERA,
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
Platform.COVER,
Platform.CAMERA,
Platform.LIGHT,
Platform.SENSOR,
]

View File

@@ -17,6 +17,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeDevice, AbodeSystem
from .const import DOMAIN
ICON = "mdi:security"
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@@ -31,6 +33,7 @@ async def async_setup_entry(
class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
"""An alarm_control_panel implementation for Abode."""
_attr_icon = ICON
_attr_name = None
_attr_code_arm_required = False
_attr_supported_features = (

View File

@@ -1,9 +0,0 @@
{
"entity": {
"switch": {
"automation": {
"default": "mdi:robot"
}
}
}
}

View File

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

View File

@@ -17,6 +17,8 @@ from .const import DOMAIN
DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE]
ICON = "mdi:robot"
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@@ -61,7 +63,7 @@ class AbodeSwitch(AbodeDevice, SwitchEntity):
class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity):
"""A switch implementation for Abode automations."""
_attr_translation_key = "automation"
_attr_icon = ICON
async def async_added_to_hass(self) -> None:
"""Set up trigger automation service."""

View File

@@ -75,7 +75,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
await hass.config_entries.async_reload(entry.entry_id)
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # pylint: disable=hass-enforce-coordinator-module
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching AccuWeather data API."""
def __init__(

View File

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

View File

@@ -66,12 +66,12 @@ class AcmedaBase(entity.Entity):
@property
def unique_id(self) -> str:
"""Return the unique ID of this roller."""
return self.roller.id # type: ignore[no-any-return]
return self.roller.id
@property
def device_id(self) -> str:
"""Return the ID of this roller."""
return self.roller.id # type: ignore[no-any-return]
return self.roller.id
@property
def device_info(self) -> dr.DeviceInfo:

View File

@@ -30,7 +30,7 @@ async def async_setup_entry(
current: set[int] = set()
@callback
def async_add_acmeda_covers() -> None:
def async_add_acmeda_covers():
async_add_acmeda_entities(
hass, AcmedaCover, config_entry, current, async_add_entities
)
@@ -95,7 +95,7 @@ class AcmedaCover(AcmedaBase, CoverEntity):
@property
def is_closed(self) -> bool:
"""Return if the cover is closed."""
return self.roller.closed_percent == 100 # type: ignore[no-any-return]
return self.roller.closed_percent == 100
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the roller."""

View File

@@ -1,8 +1,6 @@
"""Helper functions for Acmeda Pulse."""
from __future__ import annotations
from aiopulse import Roller
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
@@ -18,7 +16,7 @@ def async_add_acmeda_entities(
config_entry: ConfigEntry,
current: set[int],
async_add_entities: AddEntitiesCallback,
) -> None:
):
"""Add any new entities."""
hub = hass.data[DOMAIN][config_entry.entry_id]
LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host)
@@ -36,9 +34,7 @@ def async_add_acmeda_entities(
async_add_entities(new_items)
async def update_devices(
hass: HomeAssistant, config_entry: ConfigEntry, api: dict[int, Roller]
) -> None:
async def update_devices(hass: HomeAssistant, config_entry: ConfigEntry, api):
"""Tell hass that device info has been updated."""
dev_registry = dr.async_get(hass)

View File

@@ -2,12 +2,9 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
import aiopulse
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import ACMEDA_ENTITY_REMOVE, ACMEDA_HUB_UPDATE, LOGGER
@@ -17,29 +14,31 @@ from .helpers import update_devices
class PulseHub:
"""Manages a single Pulse Hub."""
api: aiopulse.Hub
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
def __init__(self, hass, config_entry):
"""Initialize the system."""
self.config_entry = config_entry
self.hass = hass
self.tasks: list[asyncio.Task[None]] = []
self.current_rollers: dict[int, aiopulse.Roller] = {}
self.cleanup_callbacks: list[Callable[[], None]] = []
self.api: aiopulse.Hub | None = None
self.tasks = []
self.current_rollers = {}
self.cleanup_callbacks = []
@property
def title(self) -> str:
def title(self):
"""Return the title of the hub shown in the integrations list."""
return f"{self.api.id} ({self.api.host})"
@property
def host(self) -> str:
def host(self):
"""Return the host of this hub."""
return self.config_entry.data["host"] # type: ignore[no-any-return]
return self.config_entry.data["host"]
async def async_setup(self, tries: int = 0) -> bool:
async def async_setup(self, tries=0):
"""Set up a hub based on host parameter."""
self.api = hub = aiopulse.Hub(self.host)
host = self.host
hub = aiopulse.Hub(host)
self.api = hub
hub.callback_subscribe(self.async_notify_update)
self.tasks.append(asyncio.create_task(hub.run()))
@@ -47,7 +46,7 @@ class PulseHub:
LOGGER.debug("Hub setup complete")
return True
async def async_reset(self) -> bool:
async def async_reset(self):
"""Reset this hub to default state."""
for cleanup_callback in self.cleanup_callbacks:
@@ -67,7 +66,7 @@ class PulseHub:
return True
async def async_notify_update(self, update_type: aiopulse.UpdateType) -> None:
async def async_notify_update(self, update_type):
"""Evaluate entities when hub reports that update has occurred."""
LOGGER.debug("Hub {update_type.name} updated")

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/acmeda",
"iot_class": "local_push",
"loggers": ["aiopulse"],
"requirements": ["aiopulse==0.4.4"]
"requirements": ["aiopulse==0.4.3"]
}

View File

@@ -25,7 +25,7 @@ async def async_setup_entry(
current: set[int] = set()
@callback
def async_add_acmeda_sensors() -> None:
def async_add_acmeda_sensors():
async_add_acmeda_entities(
hass, AcmedaBattery, config_entry, current, async_add_entities
)
@@ -48,4 +48,4 @@ class AcmedaBattery(AcmedaBase, SensorEntity):
@property
def native_value(self) -> float | int | None:
"""Return the state of the device."""
return self.roller.battery # type: ignore[no-any-return]
return self.roller.battery

View File

@@ -24,7 +24,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
# convert title and unique_id to string
if config_entry.version == 1:
if isinstance(config_entry.unique_id, int):
hass.config_entries.async_update_entry( # type: ignore[unreachable]
hass.config_entries.async_update_entry(
config_entry,
unique_id=str(config_entry.unique_id),
title=str(config_entry.title),

View File

@@ -67,11 +67,7 @@ class AdaxDevice(ClimateEntity):
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
_attr_max_temp = 35
_attr_min_temp = 5
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
@@ -141,7 +137,7 @@ class LocalAdaxDevice(ClimateEntity):
_attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None:
def __init__(self, adax_data_handler, unique_id):
"""Initialize the heater."""
self._adax_data_handler = adax_data_handler
self._attr_unique_id = unique_id

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
"loggers": ["adax", "adax_local"],
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
"requirements": ["adax==0.3.0", "Adax-local==0.1.5"]
}

View File

@@ -1,75 +0,0 @@
{
"entity": {
"sensor": {
"dns_queries": {
"default": "mdi:magnify"
},
"dns_queries_blocked": {
"default": "mdi:magnify-close"
},
"dns_queries_blocked_ratio": {
"default": "mdi:magnify-close"
},
"parental_control_blocked": {
"default": "mdi:human-male-girl"
},
"safe_browsing_blocked": {
"default": "mdi:shield-half-full"
},
"safe_searches_enforced": {
"default": "mdi:shield-search"
},
"average_processing_speed": {
"default": "mdi:speedometer"
},
"rules_count": {
"default": "mdi:counter"
}
},
"switch": {
"protection": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
},
"parental": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
},
"safe_search": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
},
"safe_browsing": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
},
"filtering": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
},
"query_log": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
}
}
},
"services": {
"add_url": "mdi:link-plus",
"remove_url": "mdi:link-off",
"enable_url": "mdi:link-variant",
"disable_url": "mdi:link-variant-off",
"refresh": "mdi:refresh"
}
}

View File

@@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 4
@dataclass(frozen=True, kw_only=True)
@dataclass(kw_only=True)
class AdGuardHomeEntityDescription(SensorEntityDescription):
"""Describes AdGuard Home sensor entity."""
@@ -33,48 +33,56 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
AdGuardHomeEntityDescription(
key="dns_queries",
translation_key="dns_queries",
icon="mdi:magnify",
native_unit_of_measurement="queries",
value_fn=lambda adguard: adguard.stats.dns_queries(),
),
AdGuardHomeEntityDescription(
key="blocked_filtering",
translation_key="dns_queries_blocked",
icon="mdi:magnify-close",
native_unit_of_measurement="queries",
value_fn=lambda adguard: adguard.stats.blocked_filtering(),
),
AdGuardHomeEntityDescription(
key="blocked_percentage",
translation_key="dns_queries_blocked_ratio",
icon="mdi:magnify-close",
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda adguard: adguard.stats.blocked_percentage(),
),
AdGuardHomeEntityDescription(
key="blocked_parental",
translation_key="parental_control_blocked",
icon="mdi:human-male-girl",
native_unit_of_measurement="requests",
value_fn=lambda adguard: adguard.stats.replaced_parental(),
),
AdGuardHomeEntityDescription(
key="blocked_safebrowsing",
translation_key="safe_browsing_blocked",
icon="mdi:shield-half-full",
native_unit_of_measurement="requests",
value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(),
),
AdGuardHomeEntityDescription(
key="enforced_safesearch",
translation_key="safe_searches_enforced",
icon="mdi:shield-search",
native_unit_of_measurement="requests",
value_fn=lambda adguard: adguard.stats.replaced_safesearch(),
),
AdGuardHomeEntityDescription(
key="average_speed",
translation_key="average_processing_speed",
icon="mdi:speedometer",
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
value_fn=lambda adguard: adguard.stats.avg_processing_time(),
),
AdGuardHomeEntityDescription(
key="rules_count",
translation_key="rules_count",
icon="mdi:counter",
native_unit_of_measurement="rules",
value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False),
entity_registry_enabled_default=False,

View File

@@ -21,7 +21,7 @@ SCAN_INTERVAL = timedelta(seconds=10)
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
@dataclass(kw_only=True)
class AdGuardHomeSwitchEntityDescription(SwitchEntityDescription):
"""Describes AdGuard Home switch entity."""
@@ -34,6 +34,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
AdGuardHomeSwitchEntityDescription(
key="protection",
translation_key="protection",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.protection_enabled,
turn_on_fn=lambda adguard: adguard.enable_protection,
turn_off_fn=lambda adguard: adguard.disable_protection,
@@ -41,6 +42,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
AdGuardHomeSwitchEntityDescription(
key="parental",
translation_key="parental",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.parental.enabled,
turn_on_fn=lambda adguard: adguard.parental.enable,
turn_off_fn=lambda adguard: adguard.parental.disable,
@@ -48,6 +50,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
AdGuardHomeSwitchEntityDescription(
key="safesearch",
translation_key="safe_search",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.safesearch.enabled,
turn_on_fn=lambda adguard: adguard.safesearch.enable,
turn_off_fn=lambda adguard: adguard.safesearch.disable,
@@ -55,6 +58,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
AdGuardHomeSwitchEntityDescription(
key="safebrowsing",
translation_key="safe_browsing",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.safebrowsing.enabled,
turn_on_fn=lambda adguard: adguard.safebrowsing.enable,
turn_off_fn=lambda adguard: adguard.safebrowsing.disable,
@@ -62,6 +66,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
AdGuardHomeSwitchEntityDescription(
key="filtering",
translation_key="filtering",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.filtering.enabled,
turn_on_fn=lambda adguard: adguard.filtering.enable,
turn_off_fn=lambda adguard: adguard.filtering.disable,
@@ -69,6 +74,7 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
AdGuardHomeSwitchEntityDescription(
key="querylog",
translation_key="query_log",
icon="mdi:shield-check",
is_on_fn=lambda adguard: adguard.querylog.enabled,
turn_on_fn=lambda adguard: adguard.querylog.enable,
turn_off_fn=lambda adguard: adguard.querylog.disable,

View File

@@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
@@ -19,15 +18,14 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
Platform.LIGHT,
]
_LOGGER = logging.getLogger(__name__)
REQUEST_REFRESH_DELAY = 0.5
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -53,9 +51,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
name="Advantage Air",
update_method=async_get,
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
await coordinator.async_config_entry_first_refresh()

View File

@@ -21,7 +21,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
ADVANTAGE_AIR_AUTOFAN_ENABLED,
ADVANTAGE_AIR_STATE_CLOSE,
ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON,
@@ -40,6 +39,16 @@ ADVANTAGE_AIR_HVAC_MODES = {
}
HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()}
ADVANTAGE_AIR_FAN_MODES = {
"autoAA": FAN_AUTO,
"low": FAN_LOW,
"medium": FAN_MEDIUM,
"high": FAN_HIGH,
}
HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()}
FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100}
ADVANTAGE_AIR_AUTOFAN = "aaAutoFanModeEnabled"
ADVANTAGE_AIR_MYZONE = "MyZone"
ADVANTAGE_AIR_MYAUTO = "MyAuto"
ADVANTAGE_AIR_MYAUTO_ENABLED = "myAutoModeEnabled"
@@ -47,7 +56,6 @@ ADVANTAGE_AIR_MYTEMP = "MyTemp"
ADVANTAGE_AIR_MYTEMP_ENABLED = "climateControlModeEnabled"
ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
ADVANTAGE_AIR_MYFAN = "autoAA"
PARALLEL_UPDATES = 0
@@ -77,29 +85,27 @@ async def async_setup_entry(
class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
"""AdvantageAir AC unit."""
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_AUTO]
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = PRECISION_WHOLE
_attr_max_temp = 32
_attr_min_temp = 16
_attr_name = None
_attr_hvac_modes = [
HVACMode.OFF,
HVACMode.COOL,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
HVACMode.DRY,
]
_attr_supported_features = ClimateEntityFeature.FAN_MODE
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an AdvantageAir AC unit."""
super().__init__(instance, ac_key)
self._attr_supported_features = (
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
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
@@ -112,6 +118,10 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
# MyZone
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
# Add "ezfan" mode if supported
if self._ac.get(ADVANTAGE_AIR_AUTOFAN):
self._attr_fan_modes += [FAN_AUTO]
@property
def current_temperature(self) -> float | None:
"""Return the selected zones current temperature."""
@@ -141,7 +151,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
@property
def fan_mode(self) -> str | None:
"""Return the current fan modes."""
return FAN_AUTO if self._ac["fan"] == ADVANTAGE_AIR_MYFAN else self._ac["fan"]
return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"])
@property
def target_temperature_high(self) -> float | None:
@@ -179,11 +189,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the Fan Mode."""
if fan_mode == FAN_AUTO and self._ac.get(ADVANTAGE_AIR_AUTOFAN_ENABLED):
mode = ADVANTAGE_AIR_MYFAN
else:
mode = fan_mode
await self.async_update_ac({"fan": mode})
await self.async_update_ac({"fan": HASS_FAN_MODES.get(fan_mode)})
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the Temperature."""

View File

@@ -5,4 +5,3 @@ ADVANTAGE_AIR_STATE_OPEN = "open"
ADVANTAGE_AIR_STATE_CLOSE = "close"
ADVANTAGE_AIR_STATE_ON = "on"
ADVANTAGE_AIR_STATE_OFF = "off"
ADVANTAGE_AIR_AUTOFAN_ENABLED = "aaAutoFanModeEnabled"

View File

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

View File

@@ -40,7 +40,6 @@ async def async_setup_entry(
class AdvantageAirLight(AdvantageAirEntity, LightEntity):
"""Representation of Advantage Air Light."""
_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
_attr_name = None
@@ -83,8 +82,7 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
class AdvantageAirLightDimmable(AdvantageAirLight):
"""Representation of Advantage Air Dimmable Light."""
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_attr_supported_color_modes = {ColorMode.ONOFF, ColorMode.BRIGHTNESS}
def __init__(self, instance: AdvantageAirData, light: dict[str, Any]) -> None:
"""Initialize an Advantage Air Dimmable Light."""
@@ -108,15 +106,13 @@ class AdvantageAirLightDimmable(AdvantageAirLight):
class AdvantageAirThingLight(AdvantageAirThingEntity, LightEntity):
"""Representation of Advantage Air Light controlled by myThings."""
_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
class AdvantageAirThingLightDimmable(AdvantageAirThingEntity, LightEntity):
"""Representation of Advantage Air Dimmable Light controlled by myThings."""
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_attr_supported_color_modes = {ColorMode.ONOFF, ColorMode.BRIGHTNESS}
@property
def brightness(self) -> int:

View File

@@ -7,7 +7,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
ADVANTAGE_AIR_AUTOFAN_ENABLED,
ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON,
DOMAIN as ADVANTAGE_AIR_DOMAIN,
@@ -30,8 +29,6 @@ async def async_setup_entry(
for ac_key, ac_device in aircons.items():
if ac_device["info"]["freshAirStatus"] != "none":
entities.append(AdvantageAirFreshAir(instance, ac_key))
if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]:
entities.append(AdvantageAirMyFan(instance, ac_key))
if things := instance.coordinator.data.get("myThings"):
for thing in things["things"].values():
if thing["channelDipState"] == 8: # 8 = Other relay
@@ -65,32 +62,6 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity):
await self.async_update_ac({"freshAirStatus": ADVANTAGE_AIR_STATE_OFF})
class AdvantageAirMyFan(AdvantageAirAcEntity, SwitchEntity):
"""Representation of Advantage Air MyFan control."""
_attr_icon = "mdi:fan-auto"
_attr_name = "MyFan"
_attr_device_class = SwitchDeviceClass.SWITCH
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an Advantage Air MyFan control."""
super().__init__(instance, ac_key)
self._attr_unique_id += "-myfan"
@property
def is_on(self) -> bool:
"""Return the MyFan status."""
return self._ac[ADVANTAGE_AIR_AUTOFAN_ENABLED]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn MyFan on."""
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn MyFan off."""
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: False})
class AdvantageAirRelay(AdvantageAirThingEntity, SwitchEntity):
"""Representation of Advantage Air Thing."""

View File

@@ -18,7 +18,7 @@ from .const import (
ENTRY_WEATHER_COORDINATOR,
PLATFORMS,
)
from .coordinator import WeatherUpdateCoordinator
from .weather_update_coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__)

View File

@@ -1,8 +1,6 @@
"""Config flow for AEMET OpenData."""
from __future__ import annotations
from typing import Any
from aemet_opendata.exceptions import AuthError
from aemet_opendata.interface import AEMET, ConnectionOptions
import voluptuous as vol
@@ -10,7 +8,6 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
@@ -32,9 +29,7 @@ OPTIONS_FLOW = {
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AEMET OpenData."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}

View File

@@ -59,6 +59,8 @@ ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
ATTR_API_CONDITION = "condition"
ATTR_API_FORECAST_CONDITION = "condition"
ATTR_API_FORECAST_DAILY = "forecast-daily"
ATTR_API_FORECAST_HOURLY = "forecast-hourly"
ATTR_API_FORECAST_PRECIPITATION = "precipitation"
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability"
ATTR_API_FORECAST_TEMP = "temperature"
@@ -99,6 +101,49 @@ CONDITIONS_MAP = {
AOD_COND_SUNNY: ATTR_CONDITION_SUNNY,
}
FORECAST_MONITORED_CONDITIONS = [
ATTR_API_FORECAST_CONDITION,
ATTR_API_FORECAST_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_TEMP,
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_SPEED,
]
MONITORED_CONDITIONS = [
ATTR_API_CONDITION,
ATTR_API_HUMIDITY,
ATTR_API_PRESSURE,
ATTR_API_RAIN,
ATTR_API_RAIN_PROB,
ATTR_API_SNOW,
ATTR_API_SNOW_PROB,
ATTR_API_STATION_ID,
ATTR_API_STATION_NAME,
ATTR_API_STATION_TIMESTAMP,
ATTR_API_STORM_PROB,
ATTR_API_TEMPERATURE,
ATTR_API_TEMPERATURE_FEELING,
ATTR_API_TOWN_ID,
ATTR_API_TOWN_NAME,
ATTR_API_TOWN_TIMESTAMP,
ATTR_API_WIND_BEARING,
ATTR_API_WIND_MAX_SPEED,
ATTR_API_WIND_SPEED,
]
FORECAST_MODE_DAILY = "daily"
FORECAST_MODE_HOURLY = "hourly"
FORECAST_MODES = [
FORECAST_MODE_DAILY,
FORECAST_MODE_HOURLY,
]
FORECAST_MODE_ATTR_API = {
FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY,
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
}
FORECAST_MAP = {
AOD_FORECAST_DAILY: {
AOD_CONDITION: ATTR_FORECAST_CONDITION,

View File

@@ -1,85 +0,0 @@
"""Weather data coordinator for the AEMET OpenData service."""
from __future__ import annotations
from asyncio import timeout
from datetime import timedelta
import logging
from typing import Any, Final, cast
from aemet_opendata.const import (
AOD_CONDITION,
AOD_FORECAST,
AOD_FORECAST_DAILY,
AOD_FORECAST_HOURLY,
AOD_TOWN,
)
from aemet_opendata.exceptions import AemetError
from aemet_opendata.helpers import dict_nested_value
from aemet_opendata.interface import AEMET
from homeassistant.components.weather import Forecast
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONDITIONS_MAP, DOMAIN, FORECAST_MAP
_LOGGER = logging.getLogger(__name__)
API_TIMEOUT: Final[int] = 120
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator."""
def __init__(
self,
hass: HomeAssistant,
aemet: AEMET,
) -> None:
"""Initialize coordinator."""
self.aemet = aemet
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=WEATHER_UPDATE_INTERVAL,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update coordinator data."""
async with timeout(API_TIMEOUT):
try:
await self.aemet.update()
except AemetError as error:
raise UpdateFailed(error) from error
data = self.aemet.data()
return {
"forecast": {
AOD_FORECAST_DAILY: self.aemet_forecast(data, AOD_FORECAST_DAILY),
AOD_FORECAST_HOURLY: self.aemet_forecast(data, AOD_FORECAST_HOURLY),
},
"lib": data,
}
def aemet_forecast(
self,
data: dict[str, Any],
forecast_mode: str,
) -> list[Forecast]:
"""Return the forecast array."""
forecasts = dict_nested_value(data, [AOD_TOWN, forecast_mode, AOD_FORECAST])
forecast_map = FORECAST_MAP[forecast_mode]
forecast_list: list[dict[str, Any]] = []
for forecast in forecasts:
cur_forecast: dict[str, Any] = {}
for api_key, ha_key in forecast_map.items():
value = forecast[api_key]
if api_key == AOD_CONDITION:
value = CONDITIONS_MAP.get(value)
cur_forecast[ha_key] = value
forecast_list += [cur_forecast]
return cast(list[Forecast], forecast_list)

View File

@@ -8,7 +8,7 @@ from aemet_opendata.helpers import dict_nested_value
from homeassistant.components.weather import Forecast
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import WeatherUpdateCoordinator
from .weather_update_coordinator import WeatherUpdateCoordinator
class AemetEntity(CoordinatorEntity[WeatherUpdateCoordinator]):

View File

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

View File

@@ -1,41 +1,6 @@
"""Support for the AEMET OpenData service."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Final
from aemet_opendata.const import (
AOD_CONDITION,
AOD_FEEL_TEMP,
AOD_FORECAST_CURRENT,
AOD_FORECAST_DAILY,
AOD_FORECAST_HOURLY,
AOD_HUMIDITY,
AOD_ID,
AOD_NAME,
AOD_PRECIPITATION,
AOD_PRECIPITATION_PROBABILITY,
AOD_PRESSURE,
AOD_RAIN,
AOD_RAIN_PROBABILITY,
AOD_SNOW,
AOD_SNOW_PROBABILITY,
AOD_STATION,
AOD_STORM_PROBABILITY,
AOD_TEMP,
AOD_TEMP_MAX,
AOD_TEMP_MIN,
AOD_TIMESTAMP,
AOD_TOWN,
AOD_WEATHER,
AOD_WIND_DIRECTION,
AOD_WIND_SPEED,
AOD_WIND_SPEED_MAX,
)
from aemet_opendata.helpers import dict_nested_value
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -53,6 +18,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import (
@@ -85,270 +51,172 @@ from .const import (
ATTR_API_WIND_MAX_SPEED,
ATTR_API_WIND_SPEED,
ATTRIBUTION,
CONDITIONS_MAP,
DOMAIN,
ENTRY_NAME,
ENTRY_WEATHER_COORDINATOR,
FORECAST_MODE_ATTR_API,
FORECAST_MODE_DAILY,
FORECAST_MODES,
FORECAST_MONITORED_CONDITIONS,
MONITORED_CONDITIONS,
)
from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity
from .weather_update_coordinator import WeatherUpdateCoordinator
@dataclass(frozen=True, kw_only=True)
class AemetSensorEntityDescription(SensorEntityDescription):
"""A class that describes AEMET OpenData sensor entities."""
keys: list[str] | None = None
value_fn: Callable[[str], datetime | float | int | str | None] = lambda value: value
FORECAST_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
AemetSensorEntityDescription(
key=f"forecast-daily-{ATTR_API_FORECAST_CONDITION}",
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_CONDITION],
name="Daily forecast condition",
value_fn=CONDITIONS_MAP.get,
FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=ATTR_API_FORECAST_CONDITION,
name="Condition",
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_CONDITION}",
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_CONDITION],
name="Hourly forecast condition",
value_fn=CONDITIONS_MAP.get,
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_PRECIPITATION}",
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_PRECIPITATION],
name="Hourly forecast precipitation",
SensorEntityDescription(
key=ATTR_API_FORECAST_PRECIPITATION,
name="Precipitation",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
),
AemetSensorEntityDescription(
key=f"forecast-daily-{ATTR_API_FORECAST_PRECIPITATION_PROBABILITY}",
keys=[
AOD_TOWN,
AOD_FORECAST_DAILY,
AOD_FORECAST_CURRENT,
AOD_PRECIPITATION_PROBABILITY,
],
name="Daily forecast precipitation probability",
SensorEntityDescription(
key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
name="Precipitation probability",
native_unit_of_measurement=PERCENTAGE,
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_PRECIPITATION_PROBABILITY}",
keys=[
AOD_TOWN,
AOD_FORECAST_HOURLY,
AOD_FORECAST_CURRENT,
AOD_PRECIPITATION_PROBABILITY,
],
name="Hourly forecast precipitation probability",
native_unit_of_measurement=PERCENTAGE,
),
AemetSensorEntityDescription(
key=f"forecast-daily-{ATTR_API_FORECAST_TEMP}",
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_TEMP_MAX],
name="Daily forecast temperature",
SensorEntityDescription(
key=ATTR_API_FORECAST_TEMP,
name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
),
AemetSensorEntityDescription(
key=f"forecast-daily-{ATTR_API_FORECAST_TEMP_LOW}",
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_TEMP_MIN],
name="Daily forecast temperature low",
SensorEntityDescription(
key=ATTR_API_FORECAST_TEMP_LOW,
name="Temperature Low",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_TEMP}",
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_TEMP],
name="Hourly forecast temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
),
AemetSensorEntityDescription(
key=f"forecast-daily-{ATTR_API_FORECAST_TIME}",
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_TIMESTAMP],
name="Daily forecast time",
SensorEntityDescription(
key=ATTR_API_FORECAST_TIME,
name="Time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_TIME}",
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_TIMESTAMP],
name="Hourly forecast time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
),
AemetSensorEntityDescription(
key=f"forecast-daily-{ATTR_API_FORECAST_WIND_BEARING}",
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
name="Daily forecast wind bearing",
SensorEntityDescription(
key=ATTR_API_FORECAST_WIND_BEARING,
name="Wind bearing",
native_unit_of_measurement=DEGREE,
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_WIND_BEARING}",
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
name="Hourly forecast wind bearing",
native_unit_of_measurement=DEGREE,
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_WIND_MAX_SPEED}",
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_WIND_SPEED_MAX],
name="Hourly forecast wind max speed",
SensorEntityDescription(
key=ATTR_API_FORECAST_WIND_MAX_SPEED,
name="Wind max speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
),
AemetSensorEntityDescription(
key=f"forecast-daily-{ATTR_API_FORECAST_WIND_SPEED}",
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_WIND_SPEED],
name="Daily forecast wind speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
),
AemetSensorEntityDescription(
entity_registry_enabled_default=False,
key=f"forecast-hourly-{ATTR_API_FORECAST_WIND_SPEED}",
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_WIND_SPEED],
name="Hourly forecast wind speed",
SensorEntityDescription(
key=ATTR_API_FORECAST_WIND_SPEED,
name="Wind speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
),
)
WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
AemetSensorEntityDescription(
WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=ATTR_API_CONDITION,
keys=[AOD_WEATHER, AOD_CONDITION],
name="Condition",
value_fn=CONDITIONS_MAP.get,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_HUMIDITY,
keys=[AOD_WEATHER, AOD_HUMIDITY],
name="Humidity",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_PRESSURE,
keys=[AOD_WEATHER, AOD_PRESSURE],
name="Pressure",
native_unit_of_measurement=UnitOfPressure.HPA,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_RAIN,
keys=[AOD_WEATHER, AOD_RAIN],
name="Rain",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_RAIN_PROB,
keys=[AOD_WEATHER, AOD_RAIN_PROBABILITY],
name="Rain probability",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_SNOW,
keys=[AOD_WEATHER, AOD_SNOW],
name="Snow",
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_SNOW_PROB,
keys=[AOD_WEATHER, AOD_SNOW_PROBABILITY],
name="Snow probability",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_STATION_ID,
keys=[AOD_STATION, AOD_ID],
name="Station ID",
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_STATION_NAME,
keys=[AOD_STATION, AOD_NAME],
name="Station name",
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_STATION_TIMESTAMP,
keys=[AOD_STATION, AOD_TIMESTAMP],
name="Station timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_STORM_PROB,
keys=[AOD_WEATHER, AOD_STORM_PROBABILITY],
name="Storm probability",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_TEMPERATURE,
keys=[AOD_WEATHER, AOD_TEMP],
name="Temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_TEMPERATURE_FEELING,
keys=[AOD_WEATHER, AOD_FEEL_TEMP],
name="Temperature feeling",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_TOWN_ID,
keys=[AOD_TOWN, AOD_ID],
name="Town ID",
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_TOWN_NAME,
keys=[AOD_TOWN, AOD_NAME],
name="Town name",
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_TOWN_TIMESTAMP,
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_TIMESTAMP],
name="Town timestamp",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_WIND_BEARING,
keys=[AOD_WEATHER, AOD_WIND_DIRECTION],
name="Wind bearing",
native_unit_of_measurement=DEGREE,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_WIND_MAX_SPEED,
keys=[AOD_WEATHER, AOD_WIND_SPEED_MAX],
name="Wind max speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
),
AemetSensorEntityDescription(
SensorEntityDescription(
key=ATTR_API_WIND_SPEED,
keys=[AOD_WEATHER, AOD_WIND_SPEED],
name="Wind speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
@@ -364,46 +232,108 @@ async def async_setup_entry(
) -> None:
"""Set up AEMET OpenData sensor entities based on a config entry."""
domain_data = hass.data[DOMAIN][config_entry.entry_id]
name: str = domain_data[ENTRY_NAME]
coordinator: WeatherUpdateCoordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
name = domain_data[ENTRY_NAME]
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
entities: list[AemetSensor] = []
for description in FORECAST_SENSORS + WEATHER_SENSORS:
if dict_nested_value(coordinator.data["lib"], description.keys) is not None:
entities.append(
AemetSensor(
name,
coordinator,
description,
config_entry,
)
unique_id = config_entry.unique_id
entities: list[AbstractAemetSensor] = [
AemetSensor(name, unique_id, weather_coordinator, description)
for description in WEATHER_SENSOR_TYPES
if description.key in MONITORED_CONDITIONS
]
entities.extend(
[
AemetForecastSensor(
f"{domain_data[ENTRY_NAME]} {mode} Forecast",
f"{unique_id}-forecast-{mode}",
weather_coordinator,
mode,
description,
)
for mode in FORECAST_MODES
for description in FORECAST_SENSOR_TYPES
if description.key in FORECAST_MONITORED_CONDITIONS
]
)
async_add_entities(entities)
class AemetSensor(AemetEntity, SensorEntity):
"""Implementation of an AEMET OpenData sensor."""
class AbstractAemetSensor(CoordinatorEntity[WeatherUpdateCoordinator], SensorEntity):
"""Abstract class for an AEMET OpenData sensor."""
_attr_attribution = ATTRIBUTION
entity_description: AemetSensorEntityDescription
def __init__(
self,
name: str,
name,
unique_id,
coordinator: WeatherUpdateCoordinator,
description: AemetSensorEntityDescription,
config_entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_name = f"{name} {description.name}"
self._attr_unique_id = f"{config_entry.unique_id}-{description.key}"
self._attr_unique_id = unique_id
class AemetSensor(AbstractAemetSensor):
"""Implementation of an AEMET OpenData sensor."""
def __init__(
self,
name,
unique_id_prefix,
weather_coordinator: WeatherUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(
name=name,
unique_id=f"{unique_id_prefix}-{description.key}",
coordinator=weather_coordinator,
description=description,
)
@property
def native_value(self):
"""Return the state of the device."""
value = self.get_aemet_value(self.entity_description.keys)
return self.entity_description.value_fn(value)
return self.coordinator.data.get(self.entity_description.key)
class AemetForecastSensor(AbstractAemetSensor):
"""Implementation of an AEMET OpenData forecast sensor."""
def __init__(
self,
name,
unique_id_prefix,
weather_coordinator: WeatherUpdateCoordinator,
forecast_mode,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(
name=name,
unique_id=f"{unique_id_prefix}-{description.key}",
coordinator=weather_coordinator,
description=description,
)
self._forecast_mode = forecast_mode
self._attr_entity_registry_enabled_default = (
self._forecast_mode == FORECAST_MODE_DAILY
)
@property
def native_value(self):
"""Return the state of the device."""
forecast = None
forecasts = self.coordinator.data.get(
FORECAST_MODE_ATTR_API[self._forecast_mode]
)
if forecasts:
forecast = forecasts[0].get(self.entity_description.key)
if self.entity_description.key == ATTR_API_FORECAST_TIME:
forecast = dt_util.parse_datetime(forecast)
return forecast

View File

@@ -38,8 +38,8 @@ from .const import (
ENTRY_WEATHER_COORDINATOR,
WEATHER_FORECAST_MODES,
)
from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity
from .weather_update_coordinator import WeatherUpdateCoordinator
async def async_setup_entry(

View File

@@ -0,0 +1,558 @@
"""Weather data coordinator for the AEMET OpenData service."""
from __future__ import annotations
from asyncio import timeout
from datetime import timedelta
import logging
from typing import Any, Final, cast
from aemet_opendata.const import (
AEMET_ATTR_DATE,
AEMET_ATTR_DAY,
AEMET_ATTR_DIRECTION,
AEMET_ATTR_ELABORATED,
AEMET_ATTR_FEEL_TEMPERATURE,
AEMET_ATTR_FORECAST,
AEMET_ATTR_HUMIDITY,
AEMET_ATTR_MAX,
AEMET_ATTR_MIN,
AEMET_ATTR_PRECIPITATION,
AEMET_ATTR_PRECIPITATION_PROBABILITY,
AEMET_ATTR_SKY_STATE,
AEMET_ATTR_SNOW,
AEMET_ATTR_SNOW_PROBABILITY,
AEMET_ATTR_SPEED,
AEMET_ATTR_STATION_DATE,
AEMET_ATTR_STATION_HUMIDITY,
AEMET_ATTR_STATION_PRESSURE,
AEMET_ATTR_STATION_PRESSURE_SEA,
AEMET_ATTR_STATION_TEMPERATURE,
AEMET_ATTR_STORM_PROBABILITY,
AEMET_ATTR_TEMPERATURE,
AEMET_ATTR_WIND,
AEMET_ATTR_WIND_GUST,
AOD_CONDITION,
AOD_FORECAST,
AOD_FORECAST_DAILY,
AOD_FORECAST_HOURLY,
AOD_TOWN,
ATTR_DATA,
)
from aemet_opendata.exceptions import AemetError
from aemet_opendata.forecast import ForecastValue
from aemet_opendata.helpers import (
dict_nested_value,
get_forecast_day_value,
get_forecast_hour_value,
get_forecast_interval_value,
)
from aemet_opendata.interface import AEMET
from homeassistant.components.weather import Forecast
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
ATTR_API_CONDITION,
ATTR_API_FORECAST_CONDITION,
ATTR_API_FORECAST_DAILY,
ATTR_API_FORECAST_HOURLY,
ATTR_API_FORECAST_PRECIPITATION,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_API_FORECAST_TEMP,
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY,
ATTR_API_PRESSURE,
ATTR_API_RAIN,
ATTR_API_RAIN_PROB,
ATTR_API_SNOW,
ATTR_API_SNOW_PROB,
ATTR_API_STATION_ID,
ATTR_API_STATION_NAME,
ATTR_API_STATION_TIMESTAMP,
ATTR_API_STORM_PROB,
ATTR_API_TEMPERATURE,
ATTR_API_TEMPERATURE_FEELING,
ATTR_API_TOWN_ID,
ATTR_API_TOWN_NAME,
ATTR_API_TOWN_TIMESTAMP,
ATTR_API_WIND_BEARING,
ATTR_API_WIND_MAX_SPEED,
ATTR_API_WIND_SPEED,
CONDITIONS_MAP,
DOMAIN,
FORECAST_MAP,
)
_LOGGER = logging.getLogger(__name__)
API_TIMEOUT: Final[int] = 120
STATION_MAX_DELTA = timedelta(hours=2)
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
def format_condition(condition: str) -> str:
"""Return condition from dict CONDITIONS_MAP."""
val = ForecastValue.parse_condition(condition)
return CONDITIONS_MAP.get(val, val)
def format_float(value) -> float | None:
"""Try converting string to float."""
try:
return float(value)
except (TypeError, ValueError):
return None
def format_int(value) -> int | None:
"""Try converting string to int."""
try:
return int(value)
except (TypeError, ValueError):
return None
class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator."""
def __init__(
self,
hass: HomeAssistant,
aemet: AEMET,
) -> None:
"""Initialize coordinator."""
self.aemet = aemet
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=WEATHER_UPDATE_INTERVAL,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update coordinator data."""
async with timeout(API_TIMEOUT):
try:
await self.aemet.update()
except AemetError as error:
raise UpdateFailed(error) from error
weather_response = self.aemet.legacy_weather()
return self._convert_weather_response(weather_response)
def _convert_weather_response(self, weather_response):
"""Format the weather response correctly."""
if not weather_response or not weather_response.hourly:
return None
elaborated = dt_util.parse_datetime(
weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + "Z"
)
now = dt_util.now()
now_utc = dt_util.utcnow()
hour = now.hour
# Get current day
day = None
for cur_day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
AEMET_ATTR_DAY
]:
cur_day_date = dt_util.parse_datetime(cur_day[AEMET_ATTR_DATE])
if now.date() == cur_day_date.date():
day = cur_day
break
# Get latest station data
station_data = None
station_dt = None
if weather_response.station:
for _station_data in weather_response.station[ATTR_DATA]:
if AEMET_ATTR_STATION_DATE in _station_data:
_station_dt = dt_util.parse_datetime(
_station_data[AEMET_ATTR_STATION_DATE] + "Z"
)
if not station_dt or _station_dt > station_dt:
station_data = _station_data
station_dt = _station_dt
condition = None
humidity = None
pressure = None
rain = None
rain_prob = None
snow = None
snow_prob = None
station_id = None
station_name = None
station_timestamp = None
storm_prob = None
temperature = None
temperature_feeling = None
town_id = None
town_name = None
town_timestamp = dt_util.as_utc(elaborated)
wind_bearing = None
wind_max_speed = None
wind_speed = None
# Get weather values
if day:
condition = self._get_condition(day, hour)
humidity = self._get_humidity(day, hour)
rain = self._get_rain(day, hour)
rain_prob = self._get_rain_prob(day, hour)
snow = self._get_snow(day, hour)
snow_prob = self._get_snow_prob(day, hour)
station_id = self._get_station_id()
station_name = self._get_station_name()
storm_prob = self._get_storm_prob(day, hour)
temperature = self._get_temperature(day, hour)
temperature_feeling = self._get_temperature_feeling(day, hour)
town_id = self._get_town_id()
town_name = self._get_town_name()
wind_bearing = self._get_wind_bearing(day, hour)
wind_max_speed = self._get_wind_max_speed(day, hour)
wind_speed = self._get_wind_speed(day, hour)
# Overwrite weather values with closest station data (if present)
if station_data:
station_timestamp = dt_util.as_utc(station_dt)
if (now_utc - station_dt) <= STATION_MAX_DELTA:
if AEMET_ATTR_STATION_HUMIDITY in station_data:
humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY])
if AEMET_ATTR_STATION_PRESSURE_SEA in station_data:
pressure = format_float(
station_data[AEMET_ATTR_STATION_PRESSURE_SEA]
)
elif AEMET_ATTR_STATION_PRESSURE in station_data:
pressure = format_float(station_data[AEMET_ATTR_STATION_PRESSURE])
if AEMET_ATTR_STATION_TEMPERATURE in station_data:
temperature = format_float(
station_data[AEMET_ATTR_STATION_TEMPERATURE]
)
else:
_LOGGER.warning("Station data is outdated")
# Get forecast from weather data
forecast_daily = self._get_daily_forecast_from_weather_response(
weather_response, now
)
forecast_hourly = self._get_hourly_forecast_from_weather_response(
weather_response, now
)
data = self.aemet.data()
forecasts: list[dict[str, Forecast]] = {
AOD_FORECAST_DAILY: self.aemet_forecast(data, AOD_FORECAST_DAILY),
AOD_FORECAST_HOURLY: self.aemet_forecast(data, AOD_FORECAST_HOURLY),
}
return {
ATTR_API_CONDITION: condition,
ATTR_API_FORECAST_DAILY: forecast_daily,
ATTR_API_FORECAST_HOURLY: forecast_hourly,
ATTR_API_HUMIDITY: humidity,
ATTR_API_TEMPERATURE: temperature,
ATTR_API_TEMPERATURE_FEELING: temperature_feeling,
ATTR_API_PRESSURE: pressure,
ATTR_API_RAIN: rain,
ATTR_API_RAIN_PROB: rain_prob,
ATTR_API_SNOW: snow,
ATTR_API_SNOW_PROB: snow_prob,
ATTR_API_STATION_ID: station_id,
ATTR_API_STATION_NAME: station_name,
ATTR_API_STATION_TIMESTAMP: station_timestamp,
ATTR_API_STORM_PROB: storm_prob,
ATTR_API_TOWN_ID: town_id,
ATTR_API_TOWN_NAME: town_name,
ATTR_API_TOWN_TIMESTAMP: town_timestamp,
ATTR_API_WIND_BEARING: wind_bearing,
ATTR_API_WIND_MAX_SPEED: wind_max_speed,
ATTR_API_WIND_SPEED: wind_speed,
"forecast": forecasts,
"lib": data,
}
def aemet_forecast(
self,
data: dict[str, Any],
forecast_mode: str,
) -> list[Forecast]:
"""Return the forecast array."""
forecasts = dict_nested_value(data, [AOD_TOWN, forecast_mode, AOD_FORECAST])
forecast_map = FORECAST_MAP[forecast_mode]
forecast_list: list[dict[str, Any]] = []
for forecast in forecasts:
cur_forecast: dict[str, Any] = {}
for api_key, ha_key in forecast_map.items():
value = forecast[api_key]
if api_key == AOD_CONDITION:
value = CONDITIONS_MAP.get(value)
cur_forecast[ha_key] = value
forecast_list += [cur_forecast]
return cast(list[Forecast], forecast_list)
def _get_daily_forecast_from_weather_response(self, weather_response, now):
if weather_response.daily:
parse = False
forecast = []
for day in weather_response.daily[ATTR_DATA][0][AEMET_ATTR_FORECAST][
AEMET_ATTR_DAY
]:
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
if now.date() == day_date.date():
parse = True
if parse:
cur_forecast = self._convert_forecast_day(day_date, day)
if cur_forecast:
forecast.append(cur_forecast)
return forecast
return None
def _get_hourly_forecast_from_weather_response(self, weather_response, now):
if weather_response.hourly:
parse = False
hour = now.hour
forecast = []
for day in weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_FORECAST][
AEMET_ATTR_DAY
]:
day_date = dt_util.parse_datetime(day[AEMET_ATTR_DATE])
hour_start = 0
if now.date() == day_date.date():
parse = True
hour_start = now.hour
if parse:
for hour in range(hour_start, 24):
cur_forecast = self._convert_forecast_hour(day_date, day, hour)
if cur_forecast:
forecast.append(cur_forecast)
return forecast
return None
def _convert_forecast_day(self, date, day):
if not (condition := self._get_condition_day(day)):
return None
return {
ATTR_API_FORECAST_CONDITION: condition,
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day(
day
),
ATTR_API_FORECAST_TEMP: self._get_temperature_day(day),
ATTR_API_FORECAST_TEMP_LOW: self._get_temperature_low_day(day),
ATTR_API_FORECAST_TIME: dt_util.as_utc(date).isoformat(),
ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed_day(day),
ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day),
}
def _convert_forecast_hour(self, date, day, hour):
if not (condition := self._get_condition(day, hour)):
return None
forecast_dt = date.replace(hour=hour, minute=0, second=0)
return {
ATTR_API_FORECAST_CONDITION: condition,
ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour),
ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob(
day, hour
),
ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour),
ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(),
ATTR_API_FORECAST_WIND_MAX_SPEED: self._get_wind_max_speed(day, hour),
ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
}
def _calc_precipitation(self, day, hour):
"""Calculate the precipitation."""
rain_value = self._get_rain(day, hour) or 0
snow_value = self._get_snow(day, hour) or 0
if round(rain_value + snow_value, 1) == 0:
return None
return round(rain_value + snow_value, 1)
def _calc_precipitation_prob(self, day, hour):
"""Calculate the precipitation probability (hour)."""
rain_value = self._get_rain_prob(day, hour) or 0
snow_value = self._get_snow_prob(day, hour) or 0
if rain_value == 0 and snow_value == 0:
return None
return max(rain_value, snow_value)
@staticmethod
def _get_condition(day_data, hour):
"""Get weather condition (hour) from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_SKY_STATE], hour)
if val:
return format_condition(val)
return None
@staticmethod
def _get_condition_day(day_data):
"""Get weather condition (day) from weather data."""
val = get_forecast_day_value(day_data[AEMET_ATTR_SKY_STATE])
if val:
return format_condition(val)
return None
@staticmethod
def _get_humidity(day_data, hour):
"""Get humidity from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_HUMIDITY], hour)
if val:
return format_int(val)
return None
@staticmethod
def _get_precipitation_prob_day(day_data):
"""Get humidity from weather data."""
val = get_forecast_day_value(day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY])
if val:
return format_int(val)
return None
@staticmethod
def _get_rain(day_data, hour):
"""Get rain from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_PRECIPITATION], hour)
if val:
return format_float(val)
return None
@staticmethod
def _get_rain_prob(day_data, hour):
"""Get rain probability from weather data."""
val = get_forecast_interval_value(
day_data[AEMET_ATTR_PRECIPITATION_PROBABILITY], hour
)
if val:
return format_int(val)
return None
@staticmethod
def _get_snow(day_data, hour):
"""Get snow from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_SNOW], hour)
if val:
return format_float(val)
return None
@staticmethod
def _get_snow_prob(day_data, hour):
"""Get snow probability from weather data."""
val = get_forecast_interval_value(day_data[AEMET_ATTR_SNOW_PROBABILITY], hour)
if val:
return format_int(val)
return None
def _get_station_id(self):
"""Get station ID from weather data."""
if self.aemet.station:
return self.aemet.station.get_id()
return None
def _get_station_name(self):
"""Get station name from weather data."""
if self.aemet.station:
return self.aemet.station.get_name()
return None
@staticmethod
def _get_storm_prob(day_data, hour):
"""Get storm probability from weather data."""
val = get_forecast_interval_value(day_data[AEMET_ATTR_STORM_PROBABILITY], hour)
if val:
return format_int(val)
return None
@staticmethod
def _get_temperature(day_data, hour):
"""Get temperature (hour) from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour)
return format_int(val)
@staticmethod
def _get_temperature_day(day_data):
"""Get temperature (day) from weather data."""
val = get_forecast_day_value(
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX
)
return format_int(val)
@staticmethod
def _get_temperature_low_day(day_data):
"""Get temperature (day) from weather data."""
val = get_forecast_day_value(
day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN
)
return format_int(val)
@staticmethod
def _get_temperature_feeling(day_data, hour):
"""Get temperature from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_FEEL_TEMPERATURE], hour)
return format_int(val)
def _get_town_id(self):
"""Get town ID from weather data."""
if self.aemet.town:
return self.aemet.town.get_id()
return None
def _get_town_name(self):
"""Get town name from weather data."""
if self.aemet.town:
return self.aemet.town.get_name()
return None
@staticmethod
def _get_wind_bearing(day_data, hour):
"""Get wind bearing (hour) from weather data."""
val = get_forecast_hour_value(
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION
)[0]
return ForecastValue.parse_wind_direction(val)
@staticmethod
def _get_wind_bearing_day(day_data):
"""Get wind bearing (day) from weather data."""
val = get_forecast_day_value(
day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION
)
return ForecastValue.parse_wind_direction(val)
@staticmethod
def _get_wind_max_speed(day_data, hour):
"""Get wind max speed from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_WIND_GUST], hour)
if val:
return format_int(val)
return None
@staticmethod
def _get_wind_speed(day_data, hour):
"""Get wind speed (hour) from weather data."""
val = get_forecast_hour_value(
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_SPEED
)[0]
if val:
return format_int(val)
return None
@staticmethod
def _get_wind_speed_day(day_data):
"""Get wind speed (day) from weather data."""
val = get_forecast_day_value(day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_SPEED)
if val:
return format_int(val)
return None

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@@ -51,6 +51,25 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Import configuration from yaml."""
try:
self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]})
except AbortFlow as err:
async_create_issue(
self.hass,
DOMAIN,
"deprecated_yaml_import_issue_already_configured",
breaks_in_ha_version="2024.4.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue_already_configured",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "AfterShip",
},
)
raise err
async_create_issue(
self.hass,
HOMEASSISTANT_DOMAIN,
@@ -65,8 +84,6 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN):
"integration_title": "AfterShip",
},
)
self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]})
return self.async_create_entry(
title=config.get(CONF_NAME, "AfterShip"),
data={CONF_API_KEY: config[CONF_API_KEY]},

View File

@@ -49,6 +49,10 @@
}
},
"issues": {
"deprecated_yaml_import_issue_already_configured": {
"title": "The {integration_title} YAML configuration import failed",
"description": "Configuring {integration_title} using YAML is being removed but the YAML configuration was already imported.\n\nRemove the YAML configuration and restart Home Assistant."
},
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The {integration_title} YAML configuration import failed",
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."

View File

@@ -18,6 +18,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN
ICON = "mdi:security"
CONF_HOME_MODE_NAME = "home"
CONF_AWAY_MODE_NAME = "away"
CONF_NIGHT_MODE_NAME = "night"
@@ -39,6 +41,7 @@ async def async_setup_entry(
class AgentBaseStation(AlarmControlPanelEntity):
"""Representation of an Agent DVR Alarm Control Panel."""
_attr_icon = ICON
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY

View File

@@ -1,6 +1,5 @@
"""Config flow to configure Agent devices."""
from contextlib import suppress
from typing import Any
from agent import AgentConnectionError, AgentError
from agent.a import Agent
@@ -8,7 +7,6 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, SERVER_URL
@@ -20,9 +18,11 @@ DEFAULT_PORT = 8090
class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an Agent config flow."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
def __init__(self):
"""Initialize the Agent config flow."""
self.device_config = {}
async def async_step_user(self, user_input=None):
"""Handle an Agent config flow."""
errors = {}
@@ -49,15 +49,13 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
)
device_config = {
self.device_config = {
CONF_HOST: host,
CONF_PORT: port,
SERVER_URL: server_origin,
}
return self.async_create_entry(
title=agent_client.name, data=device_config
)
return await self._create_entry(agent_client.name)
errors["base"] = "cannot_connect"
@@ -68,6 +66,11 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
description_placeholders=self.device_config,
data_schema=vol.Schema(data),
errors=errors,
)
async def _create_entry(self, server_name):
"""Create entry for device."""
return self.async_create_entry(title=server_name, data=self.device_config)

View File

@@ -6,9 +6,6 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The IP address of the Agent DVR server."
}
}
},

View File

@@ -1,7 +0,0 @@
{
"entity_component": {
"_": {
"default": "mdi:air-filter"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"entity": {
"sensor": {
"caqi": {
"default": "mdi:air-filter"
}
}
}
}

View File

@@ -56,7 +56,7 @@ from .const import (
PARALLEL_UPDATES = 1
@dataclass(frozen=True)
@dataclass
class AirlySensorEntityDescription(SensorEntityDescription):
"""Class describing Airly sensor entities."""
@@ -66,6 +66,7 @@ class AirlySensorEntityDescription(SensorEntityDescription):
SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
AirlySensorEntityDescription(
key=ATTR_API_CAQI,
icon="mdi:air-filter",
translation_key="caqi",
native_unit_of_measurement="CAQI",
suggested_display_precision=0,

View File

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

View File

@@ -1,15 +1,11 @@
"""DataUpdateCoordinator for the AirNow integration."""
from datetime import timedelta
import logging
from typing import Any
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from pyairnow import WebServiceAPI
from pyairnow.conv import aqi_to_concentration
from pyairnow.errors import AirNowError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
@@ -35,19 +31,12 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
"""The AirNow update coordinator."""
def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
latitude: float,
longitude: float,
distance: int,
update_interval: timedelta,
) -> None:
self, hass, session, api_key, latitude, longitude, distance, update_interval
):
"""Initialize."""
self.latitude = latitude
self.longitude = longitude
@@ -57,7 +46,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self) -> dict[str, Any]:
async def _async_update_data(self):
"""Update data via library."""
data = {}
try:

View File

@@ -1,18 +0,0 @@
{
"entity": {
"sensor": {
"aqi": {
"default": "mdi:blur"
},
"pm25": {
"default": "mdi:blur"
},
"o3": {
"default": "mdi:blur"
},
"station": {
"default": "mdi:blur"
}
}
}
}

View File

@@ -51,7 +51,7 @@ ATTR_LEVEL = "level"
ATTR_STATION = "reporting_station"
@dataclass(frozen=True)
@dataclass
class AirNowEntityDescriptionMixin:
"""Mixin for required keys."""
@@ -59,7 +59,7 @@ class AirNowEntityDescriptionMixin:
extra_state_attributes_fn: Callable[[Any], dict[str, str]] | None
@dataclass(frozen=True)
@dataclass
class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMixin):
"""Describes Airnow sensor entity."""
@@ -77,7 +77,7 @@ def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_AQI,
translation_key="aqi",
icon="mdi:blur",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.AQI,
value_fn=lambda data: data.get(ATTR_API_AQI),
@@ -94,7 +94,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
),
AirNowEntityDescription(
key=ATTR_API_PM25,
translation_key="pm25",
icon="mdi:blur",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM25,
@@ -104,6 +104,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_O3,
translation_key="o3",
icon="mdi:blur",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.get(ATTR_API_O3),
@@ -112,6 +113,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_STATION,
translation_key="station",
icon="mdi:blur",
value_fn=lambda data: data.get(ATTR_API_STATION),
extra_state_attributes_fn=station_extra_attrs,
),

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from typing import Any
from aioairq import AirQ, InvalidAuth
from aioairq import AirQ, InvalidAuth, InvalidInput
from aiohttp.client_exceptions import ClientConnectionError
import voluptuous as vol
@@ -42,32 +42,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
session = async_get_clientsession(self.hass)
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
try:
await airq.validate()
except ClientConnectionError:
airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session)
except InvalidInput:
_LOGGER.debug(
(
"Failed to connect to device %s. Check the IP address / device"
" ID as well as whether the device is connected to power and"
" the WiFi"
),
"%s does not appear to be a valid IP address or mDNS name",
user_input[CONF_IP_ADDRESS],
)
errors["base"] = "cannot_connect"
except InvalidAuth:
_LOGGER.debug(
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
)
errors["base"] = "invalid_auth"
errors["base"] = "invalid_input"
else:
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
try:
await airq.validate()
except ClientConnectionError:
_LOGGER.debug(
(
"Failed to connect to device %s. Check the IP address / device"
" ID as well as whether the device is connected to power and"
" the WiFi"
),
user_input[CONF_IP_ADDRESS],
)
errors["base"] = "cannot_connect"
except InvalidAuth:
_LOGGER.debug(
"Incorrect password for device %s", user_input[CONF_IP_ADDRESS]
)
errors["base"] = "invalid_auth"
else:
_LOGGER.debug(
"Successfully connected to %s", user_input[CONF_IP_ADDRESS]
)
device_info = await airq.fetch_device_info()
await self.async_set_unique_id(device_info["id"])
self._abort_if_unique_id_configured()
device_info = await airq.fetch_device_info()
await self.async_set_unique_id(device_info["id"])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=device_info["name"], data=user_input)
return self.async_create_entry(
title=device_info["name"], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors

View File

@@ -3,6 +3,7 @@ from typing import Final
DOMAIN: Final = "airq"
MANUFACTURER: Final = "CorantGmbH"
TARGET_ROUTE: Final = "average"
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
UPDATE_INTERVAL: float = 10.0

View File

@@ -13,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, MANUFACTURER, UPDATE_INTERVAL
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
_LOGGER = logging.getLogger(__name__)
@@ -56,4 +56,6 @@ class AirQCoordinator(DataUpdateCoordinator):
hw_version=info["hw_version"],
)
)
return await self.airq.get_latest_data() # type: ignore[no-any-return]
data = await self.airq.get(TARGET_ROUTE)
return self.airq.drop_uncertainties_from_data(data)

View File

@@ -1,24 +0,0 @@
{
"entity": {
"sensor": {
"health_index": {
"default": "mdi:heart-pulse"
},
"absolute_humidity": {
"default": "mdi:water"
},
"oxygen": {
"default": "mdi:leaf"
},
"performance_index": {
"default": "mdi:head-check"
},
"radon": {
"default": "mdi:radioactive"
},
"virus_index": {
"default": "mdi:virus-off"
}
}
}
}

View File

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

View File

@@ -37,14 +37,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True)
@dataclass
class AirQEntityDescriptionMixin:
"""Class for keys required by AirQ entity."""
value: Callable[[dict], float | int | None]
@dataclass(frozen=True)
@dataclass
class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin):
"""Describes AirQ sensor entity."""
@@ -190,6 +190,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
translation_key="health_index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:heart-pulse",
value=lambda data: data.get("health", 0.0) / 10.0,
),
AirQEntityDescription(
@@ -205,6 +206,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity_abs"),
icon="mdi:water",
),
AirQEntityDescription(
key="h2_M1000",
@@ -261,6 +263,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("oxygen"),
icon="mdi:leaf",
),
AirQEntityDescription(
key="o3",
@@ -274,6 +277,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
translation_key="performance_index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:head-check",
value=lambda data: data.get("performance", 0.0) / 10.0,
),
AirQEntityDescription(
@@ -289,6 +293,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm1"),
icon="mdi:dots-hexagon",
),
AirQEntityDescription(
key="pm2_5",
@@ -296,6 +301,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm2_5"),
icon="mdi:dots-hexagon",
),
AirQEntityDescription(
key="pm10",
@@ -303,6 +309,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm10"),
icon="mdi:dots-hexagon",
),
AirQEntityDescription(
key="pressure",
@@ -369,6 +376,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
native_unit_of_measurement=ACTIVITY_BECQUEREL_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("radon"),
icon="mdi:radioactive",
),
AirQEntityDescription(
key="temperature",
@@ -397,6 +405,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
translation_key="virus_index",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:virus-off",
value=lambda data: data.get("virus", 0.0),
),
]

View File

@@ -4,23 +4,21 @@ from __future__ import annotations
from datetime import timedelta
import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from airthings import Airthings, AirthingsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SECRET, DOMAIN
from .const import CONF_ID, CONF_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Airthings from a config entry."""
@@ -32,10 +30,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async_get_clientsession(hass),
)
async def _update_method() -> dict[str, AirthingsDevice]:
async def _update_method():
"""Get the latest data from Airthings."""
try:
return await airthings.update_devices() # type: ignore[no-any-return]
return await airthings.update_devices()
except AirthingsError as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err

View File

@@ -8,11 +8,10 @@ import airthings
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ID
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_SECRET, DOMAIN
from .const import CONF_ID, CONF_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)

View File

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

View File

@@ -1,10 +1,10 @@
{
"domain": "airthings",
"name": "Airthings",
"codeowners": ["@danielhiversen", "@LaStrada"],
"codeowners": ["@danielhiversen"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airthings",
"iot_class": "cloud_polling",
"loggers": ["airthings"],
"requirements": ["airthings-cloud==0.2.0"]
"requirements": ["airthings-cloud==0.1.0"]
}

View File

@@ -24,9 +24,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from . import AirthingsDataCoordinatorType
from .const import DOMAIN
SENSORS: dict[str, SensorEntityDescription] = {
@@ -106,7 +108,7 @@ async def async_setup_entry(
) -> None:
"""Set up the Airthings sensor."""
coordinator: AirthingsDataCoordinatorType = hass.data[DOMAIN][entry.entry_id]
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities = [
AirthingsHeaterEnergySensor(
coordinator,
@@ -120,9 +122,7 @@ async def async_setup_entry(
async_add_entities(entities)
class AirthingsHeaterEnergySensor(
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
):
class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity):
"""Representation of a Airthings Sensor device."""
_attr_state_class = SensorStateClass.MEASUREMENT
@@ -130,7 +130,7 @@ class AirthingsHeaterEnergySensor(
def __init__(
self,
coordinator: AirthingsDataCoordinatorType,
coordinator: DataUpdateCoordinator,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
) -> None:
@@ -149,10 +149,10 @@ class AirthingsHeaterEnergySensor(
identifiers={(DOMAIN, airthings_device.device_id)},
name=airthings_device.name,
manufacturer="Airthings",
model=airthings_device.product_name,
model=airthings_device.device_type.replace("_", " ").lower().title(),
)
@property
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
return self.coordinator.data[self._id].sensors[self.entity_description.key] # type: ignore[no-any-return]
return self.coordinator.data[self._id].sensors[self.entity_description.key]

View File

@@ -4,8 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
from bleak_retry_connector import close_stale_connections_by_address
from airthings_ble import AirthingsBluetoothDeviceData
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
@@ -31,8 +30,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
is_metric = hass.config.units is METRIC_SYSTEM
assert address is not None
await close_stale_connections_by_address(address)
ble_device = bluetooth.async_ble_device_from_address(hass, address)
if not ble_device:
@@ -40,14 +37,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Could not find Airthings device with address {address}"
)
airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric)
async def _async_update_method() -> AirthingsDevice:
async def _async_update_method():
"""Get data from Airthings BLE."""
ble_device = bluetooth.async_ble_device_from_address(hass, address)
airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric)
try:
data = await airthings.update_device(ble_device) # type: ignore[arg-type]
data = await airthings.update_device(ble_device)
except Exception as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err

View File

@@ -1,18 +0,0 @@
{
"entity": {
"sensor": {
"radon_1day_avg": {
"default": "mdi:radioactive"
},
"radon_longterm_avg": {
"default": "mdi:radioactive"
},
"radon_1day_level": {
"default": "mdi:radioactive"
},
"radon_longterm_level": {
"default": "mdi:radioactive"
}
}
}
}

View File

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

View File

@@ -1,7 +1,6 @@
"""Support for airthings ble sensors."""
from __future__ import annotations
import dataclasses
import logging
from airthings_ble import AirthingsDevice
@@ -52,20 +51,24 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
translation_key="radon_1day_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:radioactive",
),
"radon_longterm_avg": SensorEntityDescription(
key="radon_longterm_avg",
translation_key="radon_longterm_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:radioactive",
),
"radon_1day_level": SensorEntityDescription(
key="radon_1day_level",
translation_key="radon_1day_level",
icon="mdi:radioactive",
),
"radon_longterm_level": SensorEntityDescription(
key="radon_longterm_level",
translation_key="radon_longterm_level",
icon="mdi:radioactive",
),
"temperature": SensorEntityDescription(
key="temperature",
@@ -103,6 +106,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:cloud",
),
"illuminance": SensorEntityDescription(
key="illuminance",
@@ -163,13 +167,10 @@ async def async_setup_entry(
# we need to change some units
sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy()
if not is_metric:
for key, val in sensors_mapping.items():
for val in sensors_mapping.values():
if val.native_unit_of_measurement is not VOLUME_BECQUEREL:
continue
sensors_mapping[key] = dataclasses.replace(
val,
native_unit_of_measurement=VOLUME_PICOCURIE,
)
val.native_unit_of_measurement = VOLUME_PICOCURIE
entities = []
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)

View File

@@ -1,50 +0,0 @@
"""The Airtouch 5 integration."""
from __future__ import annotations
from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Airtouch 5 from a config entry."""
hass.data.setdefault(DOMAIN, {})
# Create API instance
host = entry.data[CONF_HOST]
client = Airtouch5SimpleClient(host)
# Connect to the API
try:
await client.connect_and_stay_connected()
except TimeoutError as t:
raise ConfigEntryNotReady() from t
# Store an API object for your platforms to access
hass.data[DOMAIN][entry.entry_id] = client
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):
client: Airtouch5SimpleClient = hass.data[DOMAIN][entry.entry_id]
await client.disconnect()
client.ac_status_callbacks.clear()
client.connection_state_callbacks.clear()
client.data_packet_callbacks.clear()
client.zone_status_callbacks.clear()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -1,371 +0,0 @@
"""AirTouch 5 component to control AirTouch 5 Climate Devices."""
import logging
from typing import Any
from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
from airtouch5py.packets.ac_ability import AcAbility
from airtouch5py.packets.ac_control import (
AcControl,
SetAcFanSpeed,
SetAcMode,
SetpointControl,
SetPowerSetting,
)
from airtouch5py.packets.ac_status import AcFanSpeed, AcMode, AcPowerState, AcStatus
from airtouch5py.packets.zone_control import (
ZoneControlZone,
ZoneSettingPower,
ZoneSettingValue,
)
from airtouch5py.packets.zone_name import ZoneName
from airtouch5py.packets.zone_status import ZonePowerState, ZoneStatusZone
from homeassistant.components.climate import (
FAN_AUTO,
FAN_DIFFUSE,
FAN_FOCUS,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
PRESET_BOOST,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, FAN_INTELLIGENT_AUTO, FAN_TURBO
from .entity import Airtouch5Entity
_LOGGER = logging.getLogger(__name__)
AC_MODE_TO_HVAC_MODE = {
AcMode.AUTO: HVACMode.AUTO,
AcMode.AUTO_COOL: HVACMode.AUTO,
AcMode.AUTO_HEAT: HVACMode.AUTO,
AcMode.COOL: HVACMode.COOL,
AcMode.DRY: HVACMode.DRY,
AcMode.FAN: HVACMode.FAN_ONLY,
AcMode.HEAT: HVACMode.HEAT,
}
HVAC_MODE_TO_SET_AC_MODE = {
HVACMode.AUTO: SetAcMode.SET_TO_AUTO,
HVACMode.COOL: SetAcMode.SET_TO_COOL,
HVACMode.DRY: SetAcMode.SET_TO_DRY,
HVACMode.FAN_ONLY: SetAcMode.SET_TO_FAN,
HVACMode.HEAT: SetAcMode.SET_TO_HEAT,
}
AC_FAN_SPEED_TO_FAN_SPEED = {
AcFanSpeed.AUTO: FAN_AUTO,
AcFanSpeed.QUIET: FAN_DIFFUSE,
AcFanSpeed.LOW: FAN_LOW,
AcFanSpeed.MEDIUM: FAN_MEDIUM,
AcFanSpeed.HIGH: FAN_HIGH,
AcFanSpeed.POWERFUL: FAN_FOCUS,
AcFanSpeed.TURBO: FAN_TURBO,
AcFanSpeed.INTELLIGENT_AUTO_1: FAN_INTELLIGENT_AUTO,
AcFanSpeed.INTELLIGENT_AUTO_2: FAN_INTELLIGENT_AUTO,
AcFanSpeed.INTELLIGENT_AUTO_3: FAN_INTELLIGENT_AUTO,
AcFanSpeed.INTELLIGENT_AUTO_4: FAN_INTELLIGENT_AUTO,
AcFanSpeed.INTELLIGENT_AUTO_5: FAN_INTELLIGENT_AUTO,
AcFanSpeed.INTELLIGENT_AUTO_6: FAN_INTELLIGENT_AUTO,
}
FAN_MODE_TO_SET_AC_FAN_SPEED = {
FAN_AUTO: SetAcFanSpeed.SET_TO_AUTO,
FAN_DIFFUSE: SetAcFanSpeed.SET_TO_QUIET,
FAN_LOW: SetAcFanSpeed.SET_TO_LOW,
FAN_MEDIUM: SetAcFanSpeed.SET_TO_MEDIUM,
FAN_HIGH: SetAcFanSpeed.SET_TO_HIGH,
FAN_FOCUS: SetAcFanSpeed.SET_TO_POWERFUL,
FAN_TURBO: SetAcFanSpeed.SET_TO_TURBO,
FAN_INTELLIGENT_AUTO: SetAcFanSpeed.SET_TO_INTELLIGENT_AUTO,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Airtouch 5 Climate entities."""
client: Airtouch5SimpleClient = hass.data[DOMAIN][config_entry.entry_id]
entities: list[ClimateEntity] = []
# Add each AC (and remember what zones they apply to).
# Each zone is controlled by a single AC
zone_to_ac: dict[int, AcAbility] = {}
for ac in client.ac:
for i in range(ac.start_zone_number, ac.start_zone_number + ac.zone_count):
zone_to_ac[i] = ac
entities.append(Airtouch5AC(client, ac))
# Add each zone
for zone in client.zones:
entities.append(Airtouch5Zone(client, zone, zone_to_ac[zone.zone_number]))
async_add_entities(entities)
class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
"""Base class for Airtouch5 Climate Entities."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = 1
_attr_name = None
class Airtouch5AC(Airtouch5ClimateEntity):
"""Representation of the AC unit. Used to control the overall HVAC Mode."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
def __init__(self, client: Airtouch5SimpleClient, ability: AcAbility) -> None:
"""Initialise the Climate Entity."""
super().__init__(client)
self._ability = ability
self._attr_unique_id = f"ac_{ability.ac_number}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"ac_{ability.ac_number}")},
name=f"AC {ability.ac_number}",
manufacturer="Polyaire",
model="AirTouch 5",
)
self._attr_hvac_modes = [HVACMode.OFF]
if ability.supports_mode_auto:
self._attr_hvac_modes.append(HVACMode.AUTO)
if ability.supports_mode_cool:
self._attr_hvac_modes.append(HVACMode.COOL)
if ability.supports_mode_dry:
self._attr_hvac_modes.append(HVACMode.DRY)
if ability.supports_mode_fan:
self._attr_hvac_modes.append(HVACMode.FAN_ONLY)
if ability.supports_mode_heat:
self._attr_hvac_modes.append(HVACMode.HEAT)
self._attr_fan_modes = []
if ability.supports_fan_speed_quiet:
self._attr_fan_modes.append(FAN_DIFFUSE)
if ability.supports_fan_speed_low:
self._attr_fan_modes.append(FAN_LOW)
if ability.supports_fan_speed_medium:
self._attr_fan_modes.append(FAN_MEDIUM)
if ability.supports_fan_speed_high:
self._attr_fan_modes.append(FAN_HIGH)
if ability.supports_fan_speed_powerful:
self._attr_fan_modes.append(FAN_FOCUS)
if ability.supports_fan_speed_turbo:
self._attr_fan_modes.append(FAN_TURBO)
if ability.supports_fan_speed_auto:
self._attr_fan_modes.append(FAN_AUTO)
if ability.supports_fan_speed_intelligent_auto:
self._attr_fan_modes.append(FAN_INTELLIGENT_AUTO)
# We can have different setpoints for heat cool, we expose the lowest low and highest high
self._attr_min_temp = min(
ability.min_cool_set_point, ability.min_heat_set_point
)
self._attr_max_temp = max(
ability.max_cool_set_point, ability.max_heat_set_point
)
@callback
def _async_update_attrs(self, data: dict[int, AcStatus]) -> None:
if self._ability.ac_number not in data:
return
status = data[self._ability.ac_number]
self._attr_current_temperature = status.temperature
self._attr_target_temperature = status.ac_setpoint
if status.ac_power_state in [AcPowerState.OFF, AcPowerState.AWAY_OFF]:
self._attr_hvac_mode = HVACMode.OFF
else:
self._attr_hvac_mode = AC_MODE_TO_HVAC_MODE[status.ac_mode]
self._attr_fan_mode = AC_FAN_SPEED_TO_FAN_SPEED[status.ac_fan_speed]
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Add data updated listener after this object has been initialized."""
await super().async_added_to_hass()
self._client.ac_status_callbacks.append(self._async_update_attrs)
self._async_update_attrs(self._client.latest_ac_status)
async def async_will_remove_from_hass(self) -> None:
"""Remove data updated listener after this object has been initialized."""
await super().async_will_remove_from_hass()
self._client.ac_status_callbacks.remove(self._async_update_attrs)
async def _control(
self,
*,
power: SetPowerSetting = SetPowerSetting.KEEP_POWER_SETTING,
ac_mode: SetAcMode = SetAcMode.KEEP_AC_MODE,
fan: SetAcFanSpeed = SetAcFanSpeed.KEEP_AC_FAN_SPEED,
setpoint: SetpointControl = SetpointControl.KEEP_SETPOINT_VALUE,
temp: int = 0,
) -> None:
control = AcControl(
power,
self._ability.ac_number,
ac_mode,
fan,
setpoint,
temp,
)
packet = self._client.data_packet_factory.ac_control([control])
await self._client.send_packet(packet)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new operation mode."""
set_power_setting: SetPowerSetting
set_ac_mode: SetAcMode
if hvac_mode == HVACMode.OFF:
set_power_setting = SetPowerSetting.SET_TO_OFF
set_ac_mode = SetAcMode.KEEP_AC_MODE
else:
set_power_setting = SetPowerSetting.SET_TO_ON
if hvac_mode not in HVAC_MODE_TO_SET_AC_MODE:
raise ValueError(f"Unsupported hvac mode: {hvac_mode}")
set_ac_mode = HVAC_MODE_TO_SET_AC_MODE[hvac_mode]
await self._control(power=set_power_setting, ac_mode=set_ac_mode)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new fan mode."""
if fan_mode not in FAN_MODE_TO_SET_AC_FAN_SPEED:
raise ValueError(f"Unsupported fan mode: {fan_mode}")
fan_speed = FAN_MODE_TO_SET_AC_FAN_SPEED[fan_mode]
await self._control(fan=fan_speed)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
return
await self._control(temp=temp)
class Airtouch5Zone(Airtouch5ClimateEntity):
"""Representation of a Zone. Used to control the AC effect in the zone."""
_attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY]
_attr_preset_modes = [PRESET_NONE, PRESET_BOOST]
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
def __init__(
self, client: Airtouch5SimpleClient, name: ZoneName, ac: AcAbility
) -> None:
"""Initialise the Climate Entity."""
super().__init__(client)
self._name = name
self._attr_unique_id = f"zone_{name.zone_number}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"zone_{name.zone_number}")},
name=name.zone_name,
manufacturer="Polyaire",
model="AirTouch 5",
)
# We can have different setpoints for heat and cool, we expose the lowest low and highest high
self._attr_min_temp = min(ac.min_cool_set_point, ac.min_heat_set_point)
self._attr_max_temp = max(ac.max_cool_set_point, ac.max_heat_set_point)
@callback
def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None:
if self._name.zone_number not in data:
return
status = data[self._name.zone_number]
self._attr_current_temperature = status.temperature
self._attr_target_temperature = status.set_point
if status.zone_power_state == ZonePowerState.OFF:
self._attr_hvac_mode = HVACMode.OFF
self._attr_preset_mode = PRESET_NONE
elif status.zone_power_state == ZonePowerState.ON:
self._attr_hvac_mode = HVACMode.FAN_ONLY
self._attr_preset_mode = PRESET_NONE
elif status.zone_power_state == ZonePowerState.TURBO:
self._attr_hvac_mode = HVACMode.FAN_ONLY
self._attr_preset_mode = PRESET_BOOST
else:
self._attr_hvac_mode = None
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Add data updated listener after this object has been initialized."""
await super().async_added_to_hass()
self._client.zone_status_callbacks.append(self._async_update_attrs)
self._async_update_attrs(self._client.latest_zone_status)
async def async_will_remove_from_hass(self) -> None:
"""Remove data updated listener after this object has been initialized."""
await super().async_will_remove_from_hass()
self._client.zone_status_callbacks.remove(self._async_update_attrs)
async def _control(
self,
*,
zsv: ZoneSettingValue = ZoneSettingValue.KEEP_SETTING_VALUE,
power: ZoneSettingPower = ZoneSettingPower.KEEP_POWER_STATE,
value: float = 0,
) -> None:
control = ZoneControlZone(self._name.zone_number, zsv, power, value)
packet = self._client.data_packet_factory.zone_control([control])
await self._client.send_packet(packet)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new operation mode."""
power: ZoneSettingPower
if hvac_mode is HVACMode.OFF:
power = ZoneSettingPower.SET_TO_OFF
elif self._attr_preset_mode is PRESET_BOOST:
power = ZoneSettingPower.SET_TO_TURBO
else:
power = ZoneSettingPower.SET_TO_ON
await self._control(power=power)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Enable or disable Turbo. Done this way as we can't have a turbo HVACMode."""
power: ZoneSettingPower
if preset_mode == PRESET_BOOST:
power = ZoneSettingPower.SET_TO_TURBO
else:
power = ZoneSettingPower.SET_TO_ON
await self._control(power=power)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperatures."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
return
await self._control(
zsv=ZoneSettingValue.SET_TARGET_SETPOINT,
value=float(temp),
)
async def async_turn_on(self) -> None:
"""Turn the zone on."""
await self.async_set_hvac_mode(HVACMode.FAN_ONLY)
async def async_turn_off(self) -> None:
"""Turn the zone off."""
await self.async_set_hvac_mode(HVACMode.OFF)

View File

@@ -1,46 +0,0 @@
"""Config flow for Airtouch 5 integration."""
from __future__ import annotations
import logging
from typing import Any
from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Airtouch 5."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] | None = None
if user_input is not None:
client = Airtouch5SimpleClient(user_input[CONF_HOST])
try:
await client.test_connection()
except Exception: # pylint: disable=broad-exception-caught
errors = {"base": "cannot_connect"}
else:
await self.async_set_unique_id(user_input[CONF_HOST])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_HOST], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -1,6 +0,0 @@
"""Constants for the Airtouch 5 integration."""
DOMAIN = "airtouch5"
FAN_TURBO = "turbo"
FAN_INTELLIGENT_AUTO = "intelligent_auto"

View File

@@ -1,40 +0,0 @@
"""Base class for Airtouch5 entities."""
from airtouch5py.airtouch5_client import Airtouch5ConnectionStateChange
from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class Airtouch5Entity(Entity):
"""Base class for Airtouch5 entities."""
_attr_should_poll = False
_attr_has_entity_name = True
_attr_translation_key = DOMAIN
def __init__(self, client: Airtouch5SimpleClient) -> None:
"""Initialise the Entity."""
self._client = client
self._attr_available = True
@callback
def _receive_connection_callback(
self, state: Airtouch5ConnectionStateChange
) -> None:
self._attr_available = state is Airtouch5ConnectionStateChange.CONNECTED
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Add data updated listener after this object has been initialized."""
self._client.connection_state_callbacks.append(
self._receive_connection_callback
)
async def async_will_remove_from_hass(self) -> None:
"""Remove data updated listener when entity is removed from homeassistant."""
self._client.connection_state_callbacks.remove(
self._receive_connection_callback
)

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