Compare commits

..

1 Commits

Author SHA1 Message Date
Shay Levy
628cc5dccb Update opencv-python-headless to 4.9.0.80 2024-03-13 22:41:47 +00:00
6957 changed files with 116423 additions and 303065 deletions

View File

@@ -137,7 +137,6 @@ tests: &tests
- tests/syrupy.py - tests/syrupy.py
- tests/test_util/** - tests/test_util/**
- tests/testing_config/** - tests/testing_config/**
- tests/typing.py
- tests/util/** - tests/util/**
other: &other other: &other

View File

@@ -1,17 +1,12 @@
# Sorted by hassfest.
#
# To sort, run python3 -m script.hassfest -p coverage
[run] [run]
source = homeassistant source = homeassistant
omit = omit =
homeassistant/__main__.py homeassistant/__main__.py
homeassistant/helpers/backports/aiohttp_resolver.py
homeassistant/helpers/signal.py homeassistant/helpers/signal.py
homeassistant/scripts/__init__.py homeassistant/scripts/__init__.py
homeassistant/scripts/benchmark/__init__.py
homeassistant/scripts/check_config.py homeassistant/scripts/check_config.py
homeassistant/scripts/ensure_config.py homeassistant/scripts/ensure_config.py
homeassistant/scripts/benchmark/__init__.py
homeassistant/scripts/macos/__init__.py homeassistant/scripts/macos/__init__.py
# omit pieces of code that rely on external devices being present # omit pieces of code that rely on external devices being present
@@ -58,19 +53,13 @@ omit =
homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual/sensor.py
homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/__init__.py
homeassistant/components/airvisual_pro/sensor.py homeassistant/components/airvisual_pro/sensor.py
homeassistant/components/aladdin_connect/__init__.py
homeassistant/components/aladdin_connect/api.py
homeassistant/components/aladdin_connect/application_credentials.py
homeassistant/components/aladdin_connect/cover.py
homeassistant/components/aladdin_connect/model.py
homeassistant/components/aladdin_connect/sensor.py
homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/__init__.py
homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/alarm_control_panel.py
homeassistant/components/alarmdecoder/binary_sensor.py homeassistant/components/alarmdecoder/binary_sensor.py
homeassistant/components/alarmdecoder/entity.py
homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alarmdecoder/sensor.py
homeassistant/components/alpha_vantage/sensor.py homeassistant/components/alpha_vantage/sensor.py
homeassistant/components/amazon_polly/* homeassistant/components/amazon_polly/*
homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/__init__.py homeassistant/components/ambient_station/__init__.py
homeassistant/components/ambient_station/binary_sensor.py homeassistant/components/ambient_station/binary_sensor.py
homeassistant/components/ambient_station/entity.py homeassistant/components/ambient_station/entity.py
@@ -88,9 +77,6 @@ omit =
homeassistant/components/aprilaire/climate.py homeassistant/components/aprilaire/climate.py
homeassistant/components/aprilaire/coordinator.py homeassistant/components/aprilaire/coordinator.py
homeassistant/components/aprilaire/entity.py homeassistant/components/aprilaire/entity.py
homeassistant/components/apsystems/__init__.py
homeassistant/components/apsystems/coordinator.py
homeassistant/components/apsystems/sensor.py
homeassistant/components/aqualogic/* homeassistant/components/aqualogic/*
homeassistant/components/aquostv/media_player.py homeassistant/components/aquostv/media_player.py
homeassistant/components/arcam_fmj/__init__.py homeassistant/components/arcam_fmj/__init__.py
@@ -117,8 +103,10 @@ omit =
homeassistant/components/aurora/sensor.py homeassistant/components/aurora/sensor.py
homeassistant/components/avea/light.py homeassistant/components/avea/light.py
homeassistant/components/avion/light.py homeassistant/components/avion/light.py
homeassistant/components/awair/coordinator.py homeassistant/components/azure_devops/__init__.py
homeassistant/components/azure_devops/sensor.py
homeassistant/components/azure_service_bus/* homeassistant/components/azure_service_bus/*
homeassistant/components/awair/coordinator.py
homeassistant/components/baf/__init__.py homeassistant/components/baf/__init__.py
homeassistant/components/baf/climate.py homeassistant/components/baf/climate.py
homeassistant/components/baf/entity.py homeassistant/components/baf/entity.py
@@ -129,6 +117,7 @@ omit =
homeassistant/components/baf/switch.py homeassistant/components/baf/switch.py
homeassistant/components/baidu/tts.py homeassistant/components/baidu/tts.py
homeassistant/components/bang_olufsen/__init__.py homeassistant/components/bang_olufsen/__init__.py
homeassistant/components/bang_olufsen/const.py
homeassistant/components/bang_olufsen/entity.py homeassistant/components/bang_olufsen/entity.py
homeassistant/components/bang_olufsen/media_player.py homeassistant/components/bang_olufsen/media_player.py
homeassistant/components/bang_olufsen/util.py homeassistant/components/bang_olufsen/util.py
@@ -200,8 +189,9 @@ omit =
homeassistant/components/comelit/__init__.py homeassistant/components/comelit/__init__.py
homeassistant/components/comelit/alarm_control_panel.py homeassistant/components/comelit/alarm_control_panel.py
homeassistant/components/comelit/climate.py homeassistant/components/comelit/climate.py
homeassistant/components/comelit/coordinator.py homeassistant/components/comelit/const.py
homeassistant/components/comelit/cover.py homeassistant/components/comelit/cover.py
homeassistant/components/comelit/coordinator.py
homeassistant/components/comelit/humidifier.py homeassistant/components/comelit/humidifier.py
homeassistant/components/comelit/light.py homeassistant/components/comelit/light.py
homeassistant/components/comelit/sensor.py homeassistant/components/comelit/sensor.py
@@ -212,7 +202,6 @@ omit =
homeassistant/components/control4/__init__.py homeassistant/components/control4/__init__.py
homeassistant/components/control4/director_utils.py homeassistant/components/control4/director_utils.py
homeassistant/components/control4/light.py homeassistant/components/control4/light.py
homeassistant/components/control4/media_player.py
homeassistant/components/coolmaster/coordinator.py homeassistant/components/coolmaster/coordinator.py
homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/crownstone/__init__.py homeassistant/components/crownstone/__init__.py
@@ -249,8 +238,8 @@ omit =
homeassistant/components/dominos/* homeassistant/components/dominos/*
homeassistant/components/doods/* homeassistant/components/doods/*
homeassistant/components/doorbird/__init__.py homeassistant/components/doorbird/__init__.py
homeassistant/components/doorbird/button.py
homeassistant/components/doorbird/camera.py homeassistant/components/doorbird/camera.py
homeassistant/components/doorbird/button.py
homeassistant/components/doorbird/device.py homeassistant/components/doorbird/device.py
homeassistant/components/doorbird/entity.py homeassistant/components/doorbird/entity.py
homeassistant/components/doorbird/util.py homeassistant/components/doorbird/util.py
@@ -261,18 +250,22 @@ omit =
homeassistant/components/dormakaba_dkey/lock.py homeassistant/components/dormakaba_dkey/lock.py
homeassistant/components/dormakaba_dkey/sensor.py homeassistant/components/dormakaba_dkey/sensor.py
homeassistant/components/dovado/* homeassistant/components/dovado/*
homeassistant/components/downloader/__init__.py homeassistant/components/downloader/*
homeassistant/components/dsmr_reader/__init__.py
homeassistant/components/dsmr_reader/definitions.py
homeassistant/components/dsmr_reader/sensor.py
homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dte_energy_bridge/sensor.py
homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dublin_bus_transport/sensor.py
homeassistant/components/dunehd/__init__.py homeassistant/components/dunehd/__init__.py
homeassistant/components/dunehd/media_player.py homeassistant/components/dunehd/media_player.py
homeassistant/components/duotecno/__init__.py homeassistant/components/duotecno/__init__.py
homeassistant/components/duotecno/binary_sensor.py
homeassistant/components/duotecno/climate.py
homeassistant/components/duotecno/cover.py
homeassistant/components/duotecno/entity.py homeassistant/components/duotecno/entity.py
homeassistant/components/duotecno/light.py
homeassistant/components/duotecno/switch.py homeassistant/components/duotecno/switch.py
homeassistant/components/duotecno/cover.py
homeassistant/components/duotecno/light.py
homeassistant/components/duotecno/climate.py
homeassistant/components/duotecno/binary_sensor.py
homeassistant/components/dwd_weather_warnings/const.py
homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/coordinator.py
homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dwd_weather_warnings/sensor.py
homeassistant/components/dweet/* homeassistant/components/dweet/*
@@ -311,12 +304,10 @@ omit =
homeassistant/components/edl21/__init__.py homeassistant/components/edl21/__init__.py
homeassistant/components/edl21/sensor.py homeassistant/components/edl21/sensor.py
homeassistant/components/egardia/* homeassistant/components/egardia/*
homeassistant/components/electrasmart/__init__.py
homeassistant/components/electrasmart/climate.py
homeassistant/components/electric_kiwi/__init__.py homeassistant/components/electric_kiwi/__init__.py
homeassistant/components/electric_kiwi/api.py homeassistant/components/electric_kiwi/api.py
homeassistant/components/electric_kiwi/coordinator.py
homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/oauth2.py
homeassistant/components/electric_kiwi/coordinator.py
homeassistant/components/electric_kiwi/select.py homeassistant/components/electric_kiwi/select.py
homeassistant/components/eliqonline/sensor.py homeassistant/components/eliqonline/sensor.py
homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/__init__.py
@@ -329,7 +320,8 @@ omit =
homeassistant/components/elmax/__init__.py homeassistant/components/elmax/__init__.py
homeassistant/components/elmax/alarm_control_panel.py homeassistant/components/elmax/alarm_control_panel.py
homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/binary_sensor.py
homeassistant/components/elmax/coordinator.py homeassistant/components/elmax/common.py
homeassistant/components/elmax/const.py
homeassistant/components/elmax/cover.py homeassistant/components/elmax/cover.py
homeassistant/components/elmax/switch.py homeassistant/components/elmax/switch.py
homeassistant/components/elv/* homeassistant/components/elv/*
@@ -363,17 +355,12 @@ omit =
homeassistant/components/environment_canada/weather.py homeassistant/components/environment_canada/weather.py
homeassistant/components/envisalink/* homeassistant/components/envisalink/*
homeassistant/components/ephember/climate.py homeassistant/components/ephember/climate.py
homeassistant/components/epic_games_store/__init__.py
homeassistant/components/epic_games_store/coordinator.py
homeassistant/components/epion/__init__.py homeassistant/components/epion/__init__.py
homeassistant/components/epion/coordinator.py homeassistant/components/epion/coordinator.py
homeassistant/components/epion/sensor.py homeassistant/components/epion/sensor.py
homeassistant/components/epson/__init__.py homeassistant/components/epson/__init__.py
homeassistant/components/epson/media_player.py homeassistant/components/epson/media_player.py
homeassistant/components/eq3btsmart/__init__.py homeassistant/components/epsonworkforce/sensor.py
homeassistant/components/eq3btsmart/climate.py
homeassistant/components/eq3btsmart/entity.py
homeassistant/components/eq3btsmart/models.py
homeassistant/components/escea/__init__.py homeassistant/components/escea/__init__.py
homeassistant/components/escea/climate.py homeassistant/components/escea/climate.py
homeassistant/components/escea/discovery.py homeassistant/components/escea/discovery.py
@@ -388,11 +375,11 @@ omit =
homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/binary_sensor.py
homeassistant/components/ezviz/button.py homeassistant/components/ezviz/button.py
homeassistant/components/ezviz/camera.py homeassistant/components/ezviz/camera.py
homeassistant/components/ezviz/coordinator.py
homeassistant/components/ezviz/entity.py
homeassistant/components/ezviz/image.py homeassistant/components/ezviz/image.py
homeassistant/components/ezviz/light.py homeassistant/components/ezviz/light.py
homeassistant/components/ezviz/coordinator.py
homeassistant/components/ezviz/number.py homeassistant/components/ezviz/number.py
homeassistant/components/ezviz/entity.py
homeassistant/components/ezviz/select.py homeassistant/components/ezviz/select.py
homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/sensor.py
homeassistant/components/ezviz/siren.py homeassistant/components/ezviz/siren.py
@@ -463,8 +450,8 @@ omit =
homeassistant/components/freebox/camera.py homeassistant/components/freebox/camera.py
homeassistant/components/freebox/home_base.py homeassistant/components/freebox/home_base.py
homeassistant/components/freebox/switch.py homeassistant/components/freebox/switch.py
homeassistant/components/fritz/coordinator.py homeassistant/components/fritz/common.py
homeassistant/components/fritz/entity.py homeassistant/components/fritz/device_tracker.py
homeassistant/components/fritz/services.py homeassistant/components/fritz/services.py
homeassistant/components/fritz/switch.py homeassistant/components/fritz/switch.py
homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/__init__.py
@@ -502,6 +489,7 @@ omit =
homeassistant/components/gpsd/sensor.py homeassistant/components/gpsd/sensor.py
homeassistant/components/greenwave/light.py homeassistant/components/greenwave/light.py
homeassistant/components/growatt_server/__init__.py homeassistant/components/growatt_server/__init__.py
homeassistant/components/growatt_server/const.py
homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor.py
homeassistant/components/growatt_server/sensor_types/* homeassistant/components/growatt_server/sensor_types/*
homeassistant/components/gstreamer/media_player.py homeassistant/components/gstreamer/media_player.py
@@ -515,7 +503,6 @@ omit =
homeassistant/components/guardian/util.py homeassistant/components/guardian/util.py
homeassistant/components/guardian/valve.py homeassistant/components/guardian/valve.py
homeassistant/components/habitica/__init__.py homeassistant/components/habitica/__init__.py
homeassistant/components/habitica/coordinator.py
homeassistant/components/habitica/sensor.py homeassistant/components/habitica/sensor.py
homeassistant/components/harman_kardon_avr/media_player.py homeassistant/components/harman_kardon_avr/media_player.py
homeassistant/components/harmony/data.py homeassistant/components/harmony/data.py
@@ -537,13 +524,16 @@ omit =
homeassistant/components/hive/switch.py homeassistant/components/hive/switch.py
homeassistant/components/hive/water_heater.py homeassistant/components/hive/water_heater.py
homeassistant/components/hko/__init__.py homeassistant/components/hko/__init__.py
homeassistant/components/hko/coordinator.py
homeassistant/components/hko/weather.py homeassistant/components/hko/weather.py
homeassistant/components/hko/coordinator.py
homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/__init__.py
homeassistant/components/hlk_sw16/switch.py homeassistant/components/hlk_sw16/switch.py
homeassistant/components/home_connect/__init__.py
homeassistant/components/home_connect/api.py
homeassistant/components/home_connect/binary_sensor.py homeassistant/components/home_connect/binary_sensor.py
homeassistant/components/home_connect/entity.py homeassistant/components/home_connect/entity.py
homeassistant/components/home_connect/light.py homeassistant/components/home_connect/light.py
homeassistant/components/home_connect/sensor.py
homeassistant/components/home_connect/switch.py homeassistant/components/home_connect/switch.py
homeassistant/components/homematic/__init__.py homeassistant/components/homematic/__init__.py
homeassistant/components/homematic/binary_sensor.py homeassistant/components/homematic/binary_sensor.py
@@ -555,6 +545,10 @@ omit =
homeassistant/components/homematic/notify.py homeassistant/components/homematic/notify.py
homeassistant/components/homematic/sensor.py homeassistant/components/homematic/sensor.py
homeassistant/components/homematic/switch.py homeassistant/components/homematic/switch.py
homeassistant/components/homeworks/__init__.py
homeassistant/components/homeworks/binary_sensor.py
homeassistant/components/homeworks/button.py
homeassistant/components/homeworks/light.py
homeassistant/components/horizon/media_player.py homeassistant/components/horizon/media_player.py
homeassistant/components/hp_ilo/sensor.py homeassistant/components/hp_ilo/sensor.py
homeassistant/components/huawei_lte/__init__.py homeassistant/components/huawei_lte/__init__.py
@@ -573,9 +567,9 @@ omit =
homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/sensor.py
homeassistant/components/hunterdouglas_powerview/shade_data.py homeassistant/components/hunterdouglas_powerview/shade_data.py
homeassistant/components/hunterdouglas_powerview/util.py homeassistant/components/hunterdouglas_powerview/util.py
homeassistant/components/hvv_departures/__init__.py
homeassistant/components/huum/__init__.py homeassistant/components/huum/__init__.py
homeassistant/components/huum/climate.py homeassistant/components/huum/climate.py
homeassistant/components/hvv_departures/__init__.py
homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/binary_sensor.py
homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/sensor.py
homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/ialarm/alarm_control_panel.py
@@ -668,9 +662,9 @@ omit =
homeassistant/components/keyboard/* homeassistant/components/keyboard/*
homeassistant/components/keyboard_remote/* homeassistant/components/keyboard_remote/*
homeassistant/components/keymitt_ble/__init__.py homeassistant/components/keymitt_ble/__init__.py
homeassistant/components/keymitt_ble/coordinator.py
homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/entity.py
homeassistant/components/keymitt_ble/switch.py homeassistant/components/keymitt_ble/switch.py
homeassistant/components/keymitt_ble/coordinator.py
homeassistant/components/kitchen_sink/weather.py homeassistant/components/kitchen_sink/weather.py
homeassistant/components/kiwi/lock.py homeassistant/components/kiwi/lock.py
homeassistant/components/kodi/__init__.py homeassistant/components/kodi/__init__.py
@@ -681,7 +675,6 @@ omit =
homeassistant/components/konnected/panel.py homeassistant/components/konnected/panel.py
homeassistant/components/konnected/switch.py homeassistant/components/konnected/switch.py
homeassistant/components/kostal_plenticore/__init__.py homeassistant/components/kostal_plenticore/__init__.py
homeassistant/components/kostal_plenticore/coordinator.py
homeassistant/components/kostal_plenticore/helper.py homeassistant/components/kostal_plenticore/helper.py
homeassistant/components/kostal_plenticore/select.py homeassistant/components/kostal_plenticore/select.py
homeassistant/components/kostal_plenticore/sensor.py homeassistant/components/kostal_plenticore/sensor.py
@@ -729,6 +722,7 @@ omit =
homeassistant/components/lookin/sensor.py homeassistant/components/lookin/sensor.py
homeassistant/components/loqed/sensor.py homeassistant/components/loqed/sensor.py
homeassistant/components/luci/device_tracker.py homeassistant/components/luci/device_tracker.py
homeassistant/components/luftdaten/sensor.py
homeassistant/components/lupusec/__init__.py homeassistant/components/lupusec/__init__.py
homeassistant/components/lupusec/alarm_control_panel.py homeassistant/components/lupusec/alarm_control_panel.py
homeassistant/components/lupusec/binary_sensor.py homeassistant/components/lupusec/binary_sensor.py
@@ -738,7 +732,6 @@ omit =
homeassistant/components/lutron/binary_sensor.py homeassistant/components/lutron/binary_sensor.py
homeassistant/components/lutron/cover.py homeassistant/components/lutron/cover.py
homeassistant/components/lutron/entity.py homeassistant/components/lutron/entity.py
homeassistant/components/lutron/event.py
homeassistant/components/lutron/fan.py homeassistant/components/lutron/fan.py
homeassistant/components/lutron/light.py homeassistant/components/lutron/light.py
homeassistant/components/lutron/switch.py homeassistant/components/lutron/switch.py
@@ -754,11 +747,11 @@ omit =
homeassistant/components/lyric/climate.py homeassistant/components/lyric/climate.py
homeassistant/components/lyric/sensor.py homeassistant/components/lyric/sensor.py
homeassistant/components/mailgun/notify.py homeassistant/components/mailgun/notify.py
homeassistant/components/map/*
homeassistant/components/mastodon/notify.py homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/__init__.py homeassistant/components/matrix/__init__.py
homeassistant/components/matrix/notify.py homeassistant/components/matrix/notify.py
homeassistant/components/matter/__init__.py homeassistant/components/matter/__init__.py
homeassistant/components/matter/fan.py
homeassistant/components/meater/__init__.py homeassistant/components/meater/__init__.py
homeassistant/components/meater/sensor.py homeassistant/components/meater/sensor.py
homeassistant/components/medcom_ble/__init__.py homeassistant/components/medcom_ble/__init__.py
@@ -783,17 +776,15 @@ omit =
homeassistant/components/microbees/__init__.py homeassistant/components/microbees/__init__.py
homeassistant/components/microbees/api.py homeassistant/components/microbees/api.py
homeassistant/components/microbees/application_credentials.py homeassistant/components/microbees/application_credentials.py
homeassistant/components/microbees/binary_sensor.py
homeassistant/components/microbees/button.py homeassistant/components/microbees/button.py
homeassistant/components/microbees/climate.py homeassistant/components/microbees/const.py
homeassistant/components/microbees/coordinator.py homeassistant/components/microbees/coordinator.py
homeassistant/components/microbees/cover.py
homeassistant/components/microbees/entity.py homeassistant/components/microbees/entity.py
homeassistant/components/microbees/light.py homeassistant/components/microbees/light.py
homeassistant/components/microbees/sensor.py homeassistant/components/microbees/sensor.py
homeassistant/components/microbees/switch.py homeassistant/components/microbees/switch.py
homeassistant/components/microsoft/tts.py homeassistant/components/microsoft/tts.py
homeassistant/components/mikrotik/coordinator.py homeassistant/components/mikrotik/hub.py
homeassistant/components/mill/climate.py homeassistant/components/mill/climate.py
homeassistant/components/mill/sensor.py homeassistant/components/mill/sensor.py
homeassistant/components/minio/minio_helper.py homeassistant/components/minio/minio_helper.py
@@ -804,20 +795,15 @@ omit =
homeassistant/components/mochad/switch.py homeassistant/components/mochad/switch.py
homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/button.py
homeassistant/components/modem_callerid/sensor.py homeassistant/components/modem_callerid/sensor.py
homeassistant/components/moehlenhoff_alpha2/__init__.py
homeassistant/components/moehlenhoff_alpha2/binary_sensor.py
homeassistant/components/moehlenhoff_alpha2/climate.py homeassistant/components/moehlenhoff_alpha2/climate.py
homeassistant/components/moehlenhoff_alpha2/coordinator.py homeassistant/components/moehlenhoff_alpha2/sensor.py
homeassistant/components/monzo/__init__.py
homeassistant/components/monzo/api.py
homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/__init__.py
homeassistant/components/motion_blinds/coordinator.py homeassistant/components/motion_blinds/coordinator.py
homeassistant/components/motion_blinds/cover.py homeassistant/components/motion_blinds/cover.py
homeassistant/components/motion_blinds/entity.py homeassistant/components/motion_blinds/entity.py
homeassistant/components/motion_blinds/sensor.py homeassistant/components/motion_blinds/sensor.py
homeassistant/components/motionblinds_ble/__init__.py
homeassistant/components/motionblinds_ble/button.py
homeassistant/components/motionblinds_ble/cover.py
homeassistant/components/motionblinds_ble/entity.py
homeassistant/components/motionblinds_ble/select.py
homeassistant/components/motionmount/__init__.py homeassistant/components/motionmount/__init__.py
homeassistant/components/motionmount/binary_sensor.py homeassistant/components/motionmount/binary_sensor.py
homeassistant/components/motionmount/entity.py homeassistant/components/motionmount/entity.py
@@ -843,15 +829,8 @@ omit =
homeassistant/components/mysensors/switch.py homeassistant/components/mysensors/switch.py
homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/binary_sensor.py
homeassistant/components/mystrom/light.py homeassistant/components/mystrom/light.py
homeassistant/components/mystrom/sensor.py
homeassistant/components/mystrom/switch.py homeassistant/components/mystrom/switch.py
homeassistant/components/myuplink/__init__.py homeassistant/components/mystrom/sensor.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/helpers.py
homeassistant/components/myuplink/sensor.py
homeassistant/components/nad/media_player.py homeassistant/components/nad/media_player.py
homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/__init__.py
homeassistant/components/nanoleaf/button.py homeassistant/components/nanoleaf/button.py
@@ -859,13 +838,13 @@ omit =
homeassistant/components/nanoleaf/light.py homeassistant/components/nanoleaf/light.py
homeassistant/components/neato/__init__.py homeassistant/components/neato/__init__.py
homeassistant/components/neato/api.py homeassistant/components/neato/api.py
homeassistant/components/neato/button.py
homeassistant/components/neato/camera.py homeassistant/components/neato/camera.py
homeassistant/components/neato/entity.py homeassistant/components/neato/entity.py
homeassistant/components/neato/hub.py homeassistant/components/neato/hub.py
homeassistant/components/neato/sensor.py homeassistant/components/neato/sensor.py
homeassistant/components/neato/switch.py homeassistant/components/neato/switch.py
homeassistant/components/neato/vacuum.py homeassistant/components/neato/vacuum.py
homeassistant/components/neato/button.py
homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/netdata/sensor.py homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/__init__.py homeassistant/components/netgear/__init__.py
@@ -917,8 +896,9 @@ omit =
homeassistant/components/notion/util.py homeassistant/components/notion/util.py
homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nsw_fuel_station/sensor.py
homeassistant/components/nuki/__init__.py homeassistant/components/nuki/__init__.py
homeassistant/components/nuki/coordinator.py homeassistant/components/nuki/binary_sensor.py
homeassistant/components/nuki/lock.py homeassistant/components/nuki/lock.py
homeassistant/components/nuki/sensor.py
homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nx584/alarm_control_panel.py
homeassistant/components/oasa_telematics/sensor.py homeassistant/components/oasa_telematics/sensor.py
homeassistant/components/obihai/__init__.py homeassistant/components/obihai/__init__.py
@@ -931,12 +911,11 @@ omit =
homeassistant/components/ohmconnect/sensor.py homeassistant/components/ohmconnect/sensor.py
homeassistant/components/ombi/* homeassistant/components/ombi/*
homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/__init__.py
homeassistant/components/omnilogic/coordinator.py homeassistant/components/omnilogic/common.py
homeassistant/components/omnilogic/sensor.py homeassistant/components/omnilogic/sensor.py
homeassistant/components/omnilogic/switch.py homeassistant/components/omnilogic/switch.py
homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/__init__.py
homeassistant/components/ondilo_ico/api.py homeassistant/components/ondilo_ico/api.py
homeassistant/components/ondilo_ico/coordinator.py
homeassistant/components/ondilo_ico/sensor.py homeassistant/components/ondilo_ico/sensor.py
homeassistant/components/onkyo/media_player.py homeassistant/components/onkyo/media_player.py
homeassistant/components/onvif/__init__.py homeassistant/components/onvif/__init__.py
@@ -948,6 +927,7 @@ omit =
homeassistant/components/onvif/sensor.py homeassistant/components/onvif/sensor.py
homeassistant/components/onvif/util.py homeassistant/components/onvif/util.py
homeassistant/components/open_meteo/weather.py homeassistant/components/open_meteo/weather.py
homeassistant/components/opencv/*
homeassistant/components/openevse/sensor.py homeassistant/components/openevse/sensor.py
homeassistant/components/openexchangerates/__init__.py homeassistant/components/openexchangerates/__init__.py
homeassistant/components/openexchangerates/coordinator.py homeassistant/components/openexchangerates/coordinator.py
@@ -959,6 +939,7 @@ omit =
homeassistant/components/opengarage/sensor.py homeassistant/components/opengarage/sensor.py
homeassistant/components/openhardwaremonitor/sensor.py homeassistant/components/openhardwaremonitor/sensor.py
homeassistant/components/openhome/__init__.py homeassistant/components/openhome/__init__.py
homeassistant/components/openhome/const.py
homeassistant/components/openhome/media_player.py homeassistant/components/openhome/media_player.py
homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensensemap/air_quality.py
homeassistant/components/opentherm_gw/__init__.py homeassistant/components/opentherm_gw/__init__.py
@@ -969,22 +950,18 @@ omit =
homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/binary_sensor.py
homeassistant/components/openuv/coordinator.py homeassistant/components/openuv/coordinator.py
homeassistant/components/openuv/sensor.py homeassistant/components/openuv/sensor.py
homeassistant/components/openweathermap/__init__.py
homeassistant/components/openweathermap/coordinator.py
homeassistant/components/openweathermap/repairs.py
homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/sensor.py
homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py
homeassistant/components/opnsense/__init__.py homeassistant/components/opnsense/__init__.py
homeassistant/components/opnsense/device_tracker.py
homeassistant/components/opower/__init__.py homeassistant/components/opower/__init__.py
homeassistant/components/opower/coordinator.py homeassistant/components/opower/coordinator.py
homeassistant/components/opower/sensor.py homeassistant/components/opower/sensor.py
homeassistant/components/opnsense/device_tracker.py
homeassistant/components/opple/light.py homeassistant/components/opple/light.py
homeassistant/components/oru/* homeassistant/components/oru/*
homeassistant/components/orvibo/switch.py homeassistant/components/orvibo/switch.py
homeassistant/components/osoenergy/__init__.py homeassistant/components/osoenergy/__init__.py
homeassistant/components/osoenergy/binary_sensor.py homeassistant/components/osoenergy/const.py
homeassistant/components/osoenergy/sensor.py
homeassistant/components/osoenergy/water_heater.py homeassistant/components/osoenergy/water_heater.py
homeassistant/components/osramlightify/light.py homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py homeassistant/components/otp/sensor.py
@@ -1020,7 +997,6 @@ omit =
homeassistant/components/permobil/entity.py homeassistant/components/permobil/entity.py
homeassistant/components/permobil/sensor.py homeassistant/components/permobil/sensor.py
homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/__init__.py
homeassistant/components/philips_js/coordinator.py
homeassistant/components/philips_js/light.py homeassistant/components/philips_js/light.py
homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/media_player.py
homeassistant/components/philips_js/remote.py homeassistant/components/philips_js/remote.py
@@ -1029,6 +1005,7 @@ omit =
homeassistant/components/picotts/tts.py homeassistant/components/picotts/tts.py
homeassistant/components/pilight/base_class.py homeassistant/components/pilight/base_class.py
homeassistant/components/pilight/binary_sensor.py homeassistant/components/pilight/binary_sensor.py
homeassistant/components/pilight/const.py
homeassistant/components/pilight/light.py homeassistant/components/pilight/light.py
homeassistant/components/pilight/switch.py homeassistant/components/pilight/switch.py
homeassistant/components/ping/__init__.py homeassistant/components/ping/__init__.py
@@ -1047,6 +1024,11 @@ omit =
homeassistant/components/point/alarm_control_panel.py homeassistant/components/point/alarm_control_panel.py
homeassistant/components/point/binary_sensor.py homeassistant/components/point/binary_sensor.py
homeassistant/components/point/sensor.py homeassistant/components/point/sensor.py
homeassistant/components/poolsense/__init__.py
homeassistant/components/poolsense/binary_sensor.py
homeassistant/components/poolsense/coordinator.py
homeassistant/components/poolsense/entity.py
homeassistant/components/poolsense/sensor.py
homeassistant/components/powerwall/__init__.py homeassistant/components/powerwall/__init__.py
homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/__init__.py
homeassistant/components/progettihwsw/binary_sensor.py homeassistant/components/progettihwsw/binary_sensor.py
@@ -1073,12 +1055,12 @@ omit =
homeassistant/components/quantum_gateway/device_tracker.py homeassistant/components/quantum_gateway/device_tracker.py
homeassistant/components/qvr_pro/* homeassistant/components/qvr_pro/*
homeassistant/components/rabbitair/__init__.py homeassistant/components/rabbitair/__init__.py
homeassistant/components/rabbitair/const.py
homeassistant/components/rabbitair/coordinator.py homeassistant/components/rabbitair/coordinator.py
homeassistant/components/rabbitair/entity.py homeassistant/components/rabbitair/entity.py
homeassistant/components/rabbitair/fan.py homeassistant/components/rabbitair/fan.py
homeassistant/components/rachio/__init__.py homeassistant/components/rachio/__init__.py
homeassistant/components/rachio/binary_sensor.py homeassistant/components/rachio/binary_sensor.py
homeassistant/components/rachio/coordinator.py
homeassistant/components/rachio/device.py homeassistant/components/rachio/device.py
homeassistant/components/rachio/entity.py homeassistant/components/rachio/entity.py
homeassistant/components/rachio/switch.py homeassistant/components/rachio/switch.py
@@ -1095,12 +1077,22 @@ omit =
homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/__init__.py
homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/binary_sensor.py
homeassistant/components/rainmachine/button.py homeassistant/components/rainmachine/button.py
homeassistant/components/rainmachine/coordinator.py
homeassistant/components/rainmachine/select.py homeassistant/components/rainmachine/select.py
homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/sensor.py
homeassistant/components/rainmachine/switch.py homeassistant/components/rainmachine/switch.py
homeassistant/components/rainmachine/update.py homeassistant/components/rainmachine/update.py
homeassistant/components/rainmachine/util.py homeassistant/components/rainmachine/util.py
homeassistant/components/renson/__init__.py
homeassistant/components/renson/const.py
homeassistant/components/renson/coordinator.py
homeassistant/components/renson/entity.py
homeassistant/components/renson/sensor.py
homeassistant/components/renson/button.py
homeassistant/components/renson/fan.py
homeassistant/components/renson/switch.py
homeassistant/components/renson/binary_sensor.py
homeassistant/components/renson/number.py
homeassistant/components/renson/time.py
homeassistant/components/raspyrfm/* homeassistant/components/raspyrfm/*
homeassistant/components/recollect_waste/sensor.py homeassistant/components/recollect_waste/sensor.py
homeassistant/components/recorder/repack.py homeassistant/components/recorder/repack.py
@@ -1115,16 +1107,6 @@ omit =
homeassistant/components/rejseplanen/sensor.py homeassistant/components/rejseplanen/sensor.py
homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote_rpi_gpio/* homeassistant/components/remote_rpi_gpio/*
homeassistant/components/renson/__init__.py
homeassistant/components/renson/binary_sensor.py
homeassistant/components/renson/button.py
homeassistant/components/renson/coordinator.py
homeassistant/components/renson/entity.py
homeassistant/components/renson/fan.py
homeassistant/components/renson/number.py
homeassistant/components/renson/sensor.py
homeassistant/components/renson/switch.py
homeassistant/components/renson/time.py
homeassistant/components/reolink/binary_sensor.py homeassistant/components/reolink/binary_sensor.py
homeassistant/components/reolink/button.py homeassistant/components/reolink/button.py
homeassistant/components/reolink/camera.py homeassistant/components/reolink/camera.py
@@ -1150,10 +1132,7 @@ omit =
homeassistant/components/roborock/coordinator.py homeassistant/components/roborock/coordinator.py
homeassistant/components/rocketchat/notify.py homeassistant/components/rocketchat/notify.py
homeassistant/components/romy/__init__.py homeassistant/components/romy/__init__.py
homeassistant/components/romy/binary_sensor.py
homeassistant/components/romy/coordinator.py homeassistant/components/romy/coordinator.py
homeassistant/components/romy/entity.py
homeassistant/components/romy/sensor.py
homeassistant/components/romy/vacuum.py homeassistant/components/romy/vacuum.py
homeassistant/components/roomba/__init__.py homeassistant/components/roomba/__init__.py
homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/binary_sensor.py
@@ -1168,18 +1147,18 @@ omit =
homeassistant/components/roon/media_player.py homeassistant/components/roon/media_player.py
homeassistant/components/roon/server.py homeassistant/components/roon/server.py
homeassistant/components/route53/* homeassistant/components/route53/*
homeassistant/components/rova/sensor.py
homeassistant/components/rpi_camera/* homeassistant/components/rpi_camera/*
homeassistant/components/rtorrent/sensor.py homeassistant/components/rtorrent/sensor.py
homeassistant/components/russound_rio/media_player.py
homeassistant/components/russound_rnet/media_player.py
homeassistant/components/ruuvi_gateway/__init__.py homeassistant/components/ruuvi_gateway/__init__.py
homeassistant/components/ruuvi_gateway/bluetooth.py homeassistant/components/ruuvi_gateway/bluetooth.py
homeassistant/components/ruuvi_gateway/coordinator.py homeassistant/components/ruuvi_gateway/coordinator.py
homeassistant/components/russound_rio/media_player.py
homeassistant/components/russound_rnet/media_player.py
homeassistant/components/rympro/__init__.py homeassistant/components/rympro/__init__.py
homeassistant/components/rympro/coordinator.py homeassistant/components/rympro/coordinator.py
homeassistant/components/rympro/sensor.py homeassistant/components/rympro/sensor.py
homeassistant/components/sabnzbd/__init__.py homeassistant/components/sabnzbd/__init__.py
homeassistant/components/sabnzbd/coordinator.py
homeassistant/components/sabnzbd/sensor.py homeassistant/components/sabnzbd/sensor.py
homeassistant/components/saj/sensor.py homeassistant/components/saj/sensor.py
homeassistant/components/satel_integra/* homeassistant/components/satel_integra/*
@@ -1187,10 +1166,12 @@ omit =
homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/binary_sensor.py
homeassistant/components/screenlogic/climate.py homeassistant/components/screenlogic/climate.py
homeassistant/components/screenlogic/coordinator.py homeassistant/components/screenlogic/coordinator.py
homeassistant/components/screenlogic/const.py
homeassistant/components/screenlogic/entity.py homeassistant/components/screenlogic/entity.py
homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/light.py
homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/number.py
homeassistant/components/screenlogic/sensor.py homeassistant/components/screenlogic/sensor.py
homeassistant/components/screenlogic/services.py
homeassistant/components/screenlogic/switch.py homeassistant/components/screenlogic/switch.py
homeassistant/components/scsgate/* homeassistant/components/scsgate/*
homeassistant/components/sendgrid/notify.py homeassistant/components/sendgrid/notify.py
@@ -1243,6 +1224,7 @@ omit =
homeassistant/components/smappee/switch.py homeassistant/components/smappee/switch.py
homeassistant/components/smarty/* homeassistant/components/smarty/*
homeassistant/components/sms/__init__.py homeassistant/components/sms/__init__.py
homeassistant/components/sms/const.py
homeassistant/components/sms/coordinator.py homeassistant/components/sms/coordinator.py
homeassistant/components/sms/gateway.py homeassistant/components/sms/gateway.py
homeassistant/components/sms/notify.py homeassistant/components/sms/notify.py
@@ -1259,8 +1241,8 @@ omit =
homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge/coordinator.py
homeassistant/components/solaredge_local/sensor.py homeassistant/components/solaredge_local/sensor.py
homeassistant/components/solarlog/__init__.py homeassistant/components/solarlog/__init__.py
homeassistant/components/solarlog/coordinator.py
homeassistant/components/solarlog/sensor.py homeassistant/components/solarlog/sensor.py
homeassistant/components/solarlog/coordinator.py
homeassistant/components/solax/__init__.py homeassistant/components/solax/__init__.py
homeassistant/components/solax/sensor.py homeassistant/components/solax/sensor.py
homeassistant/components/soma/__init__.py homeassistant/components/soma/__init__.py
@@ -1297,6 +1279,13 @@ omit =
homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/browse_media.py
homeassistant/components/squeezebox/media_player.py homeassistant/components/squeezebox/media_player.py
homeassistant/components/starlink/__init__.py
homeassistant/components/starlink/binary_sensor.py
homeassistant/components/starlink/button.py
homeassistant/components/starlink/coordinator.py
homeassistant/components/starlink/device_tracker.py
homeassistant/components/starlink/sensor.py
homeassistant/components/starlink/switch.py
homeassistant/components/starline/__init__.py homeassistant/components/starline/__init__.py
homeassistant/components/starline/account.py homeassistant/components/starline/account.py
homeassistant/components/starline/binary_sensor.py homeassistant/components/starline/binary_sensor.py
@@ -1307,14 +1296,6 @@ omit =
homeassistant/components/starline/sensor.py homeassistant/components/starline/sensor.py
homeassistant/components/starline/switch.py homeassistant/components/starline/switch.py
homeassistant/components/starlingbank/sensor.py homeassistant/components/starlingbank/sensor.py
homeassistant/components/starlink/__init__.py
homeassistant/components/starlink/binary_sensor.py
homeassistant/components/starlink/button.py
homeassistant/components/starlink/coordinator.py
homeassistant/components/starlink/device_tracker.py
homeassistant/components/starlink/sensor.py
homeassistant/components/starlink/switch.py
homeassistant/components/starlink/time.py
homeassistant/components/steam_online/sensor.py homeassistant/components/steam_online/sensor.py
homeassistant/components/stiebel_eltron/* homeassistant/components/stiebel_eltron/*
homeassistant/components/stookalert/__init__.py homeassistant/components/stookalert/__init__.py
@@ -1336,7 +1317,6 @@ omit =
homeassistant/components/supla/* homeassistant/components/supla/*
homeassistant/components/surepetcare/__init__.py homeassistant/components/surepetcare/__init__.py
homeassistant/components/surepetcare/binary_sensor.py homeassistant/components/surepetcare/binary_sensor.py
homeassistant/components/surepetcare/coordinator.py
homeassistant/components/surepetcare/entity.py homeassistant/components/surepetcare/entity.py
homeassistant/components/surepetcare/sensor.py homeassistant/components/surepetcare/sensor.py
homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py
@@ -1359,13 +1339,12 @@ omit =
homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/entity.py
homeassistant/components/switchbot/humidifier.py homeassistant/components/switchbot/humidifier.py
homeassistant/components/switchbot/light.py homeassistant/components/switchbot/light.py
homeassistant/components/switchbot/lock.py
homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/sensor.py
homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/switch.py
homeassistant/components/switchbot/lock.py
homeassistant/components/switchbot_cloud/climate.py homeassistant/components/switchbot_cloud/climate.py
homeassistant/components/switchbot_cloud/coordinator.py homeassistant/components/switchbot_cloud/coordinator.py
homeassistant/components/switchbot_cloud/entity.py homeassistant/components/switchbot_cloud/entity.py
homeassistant/components/switchbot_cloud/sensor.py
homeassistant/components/switchbot_cloud/switch.py homeassistant/components/switchbot_cloud/switch.py
homeassistant/components/switchmate/switch.py homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/__init__.py
@@ -1402,6 +1381,11 @@ omit =
homeassistant/components/tado/water_heater.py homeassistant/components/tado/water_heater.py
homeassistant/components/tami4/button.py homeassistant/components/tami4/button.py
homeassistant/components/tank_utility/sensor.py homeassistant/components/tank_utility/sensor.py
homeassistant/components/tankerkoenig/__init__.py
homeassistant/components/tankerkoenig/binary_sensor.py
homeassistant/components/tankerkoenig/coordinator.py
homeassistant/components/tankerkoenig/entity.py
homeassistant/components/tankerkoenig/sensor.py
homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tapsaff/binary_sensor.py
homeassistant/components/tautulli/__init__.py homeassistant/components/tautulli/__init__.py
homeassistant/components/tautulli/coordinator.py homeassistant/components/tautulli/coordinator.py
@@ -1424,11 +1408,12 @@ omit =
homeassistant/components/tensorflow/image_processing.py homeassistant/components/tensorflow/image_processing.py
homeassistant/components/tfiac/climate.py homeassistant/components/tfiac/climate.py
homeassistant/components/thermoworks_smoke/sensor.py homeassistant/components/thermoworks_smoke/sensor.py
homeassistant/components/thethingsnetwork/*
homeassistant/components/thingspeak/* homeassistant/components/thingspeak/*
homeassistant/components/thinkingcleaner/* homeassistant/components/thinkingcleaner/*
homeassistant/components/thomson/device_tracker.py homeassistant/components/thomson/device_tracker.py
homeassistant/components/tibber/__init__.py homeassistant/components/tibber/__init__.py
homeassistant/components/tibber/coordinator.py homeassistant/components/tibber/notify.py
homeassistant/components/tibber/sensor.py homeassistant/components/tibber/sensor.py
homeassistant/components/tikteck/light.py homeassistant/components/tikteck/light.py
homeassistant/components/tile/__init__.py homeassistant/components/tile/__init__.py
@@ -1445,7 +1430,6 @@ omit =
homeassistant/components/tolo/number.py homeassistant/components/tolo/number.py
homeassistant/components/tolo/select.py homeassistant/components/tolo/select.py
homeassistant/components/tolo/sensor.py homeassistant/components/tolo/sensor.py
homeassistant/components/tolo/switch.py
homeassistant/components/toon/__init__.py homeassistant/components/toon/__init__.py
homeassistant/components/toon/binary_sensor.py homeassistant/components/toon/binary_sensor.py
homeassistant/components/toon/climate.py homeassistant/components/toon/climate.py
@@ -1469,7 +1453,6 @@ omit =
homeassistant/components/traccar_server/device_tracker.py homeassistant/components/traccar_server/device_tracker.py
homeassistant/components/traccar_server/entity.py homeassistant/components/traccar_server/entity.py
homeassistant/components/traccar_server/helpers.py homeassistant/components/traccar_server/helpers.py
homeassistant/components/traccar_server/sensor.py
homeassistant/components/tractive/__init__.py homeassistant/components/tractive/__init__.py
homeassistant/components/tractive/binary_sensor.py homeassistant/components/tractive/binary_sensor.py
homeassistant/components/tractive/device_tracker.py homeassistant/components/tractive/device_tracker.py
@@ -1517,9 +1500,9 @@ omit =
homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ue_smart_radio/media_player.py
homeassistant/components/ukraine_alarm/__init__.py homeassistant/components/ukraine_alarm/__init__.py
homeassistant/components/ukraine_alarm/binary_sensor.py homeassistant/components/ukraine_alarm/binary_sensor.py
homeassistant/components/unifiled/*
homeassistant/components/unifi_direct/__init__.py homeassistant/components/unifi_direct/__init__.py
homeassistant/components/unifi_direct/device_tracker.py homeassistant/components/unifi_direct/device_tracker.py
homeassistant/components/unifiled/*
homeassistant/components/upb/__init__.py homeassistant/components/upb/__init__.py
homeassistant/components/upb/light.py homeassistant/components/upb/light.py
homeassistant/components/upc_connect/* homeassistant/components/upc_connect/*
@@ -1529,6 +1512,7 @@ omit =
homeassistant/components/upnp/__init__.py homeassistant/components/upnp/__init__.py
homeassistant/components/upnp/device.py homeassistant/components/upnp/device.py
homeassistant/components/upnp/sensor.py homeassistant/components/upnp/sensor.py
homeassistant/components/vasttrafik/sensor.py
homeassistant/components/v2c/__init__.py homeassistant/components/v2c/__init__.py
homeassistant/components/v2c/binary_sensor.py homeassistant/components/v2c/binary_sensor.py
homeassistant/components/v2c/coordinator.py homeassistant/components/v2c/coordinator.py
@@ -1536,9 +1520,6 @@ omit =
homeassistant/components/v2c/number.py homeassistant/components/v2c/number.py
homeassistant/components/v2c/sensor.py homeassistant/components/v2c/sensor.py
homeassistant/components/v2c/switch.py homeassistant/components/v2c/switch.py
homeassistant/components/vallox/__init__.py
homeassistant/components/vallox/coordinator.py
homeassistant/components/vasttrafik/sensor.py
homeassistant/components/velbus/__init__.py homeassistant/components/velbus/__init__.py
homeassistant/components/velbus/binary_sensor.py homeassistant/components/velbus/binary_sensor.py
homeassistant/components/velbus/button.py homeassistant/components/velbus/button.py
@@ -1546,14 +1527,15 @@ omit =
homeassistant/components/velbus/cover.py homeassistant/components/velbus/cover.py
homeassistant/components/velbus/entity.py homeassistant/components/velbus/entity.py
homeassistant/components/velbus/light.py homeassistant/components/velbus/light.py
homeassistant/components/velbus/select.py
homeassistant/components/velbus/sensor.py homeassistant/components/velbus/sensor.py
homeassistant/components/velbus/select.py
homeassistant/components/velbus/switch.py homeassistant/components/velbus/switch.py
homeassistant/components/velux/__init__.py homeassistant/components/velux/__init__.py
homeassistant/components/velux/cover.py homeassistant/components/velux/cover.py
homeassistant/components/velux/light.py homeassistant/components/velux/light.py
homeassistant/components/venstar/__init__.py
homeassistant/components/venstar/binary_sensor.py
homeassistant/components/venstar/climate.py homeassistant/components/venstar/climate.py
homeassistant/components/venstar/coordinator.py
homeassistant/components/venstar/sensor.py homeassistant/components/venstar/sensor.py
homeassistant/components/verisure/__init__.py homeassistant/components/verisure/__init__.py
homeassistant/components/verisure/alarm_control_panel.py homeassistant/components/verisure/alarm_control_panel.py
@@ -1586,6 +1568,7 @@ omit =
homeassistant/components/vlc_telnet/media_player.py homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/vodafone_station/__init__.py homeassistant/components/vodafone_station/__init__.py
homeassistant/components/vodafone_station/button.py homeassistant/components/vodafone_station/button.py
homeassistant/components/vodafone_station/const.py
homeassistant/components/vodafone_station/coordinator.py homeassistant/components/vodafone_station/coordinator.py
homeassistant/components/vodafone_station/device_tracker.py homeassistant/components/vodafone_station/device_tracker.py
homeassistant/components/vodafone_station/sensor.py homeassistant/components/vodafone_station/sensor.py
@@ -1610,8 +1593,10 @@ omit =
homeassistant/components/watttime/__init__.py homeassistant/components/watttime/__init__.py
homeassistant/components/watttime/sensor.py homeassistant/components/watttime/sensor.py
homeassistant/components/weatherflow/__init__.py homeassistant/components/weatherflow/__init__.py
homeassistant/components/weatherflow/const.py
homeassistant/components/weatherflow/sensor.py homeassistant/components/weatherflow/sensor.py
homeassistant/components/weatherflow_cloud/__init__.py homeassistant/components/weatherflow_cloud/__init__.py
homeassistant/components/weatherflow_cloud/const.py
homeassistant/components/weatherflow_cloud/coordinator.py homeassistant/components/weatherflow_cloud/coordinator.py
homeassistant/components/weatherflow_cloud/weather.py homeassistant/components/weatherflow_cloud/weather.py
homeassistant/components/wiffi/__init__.py homeassistant/components/wiffi/__init__.py
@@ -1629,7 +1614,6 @@ omit =
homeassistant/components/xbox/base_sensor.py homeassistant/components/xbox/base_sensor.py
homeassistant/components/xbox/binary_sensor.py homeassistant/components/xbox/binary_sensor.py
homeassistant/components/xbox/browse_media.py homeassistant/components/xbox/browse_media.py
homeassistant/components/xbox/coordinator.py
homeassistant/components/xbox/media_player.py homeassistant/components/xbox/media_player.py
homeassistant/components/xbox/remote.py homeassistant/components/xbox/remote.py
homeassistant/components/xbox/sensor.py homeassistant/components/xbox/sensor.py
@@ -1662,7 +1646,10 @@ omit =
homeassistant/components/xs1/* homeassistant/components/xs1/*
homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/__init__.py
homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py
homeassistant/components/yale_smart_alarm/binary_sensor.py
homeassistant/components/yale_smart_alarm/button.py
homeassistant/components/yale_smart_alarm/entity.py homeassistant/components/yale_smart_alarm/entity.py
homeassistant/components/yale_smart_alarm/lock.py
homeassistant/components/yalexs_ble/__init__.py homeassistant/components/yalexs_ble/__init__.py
homeassistant/components/yalexs_ble/binary_sensor.py homeassistant/components/yalexs_ble/binary_sensor.py
homeassistant/components/yalexs_ble/entity.py homeassistant/components/yalexs_ble/entity.py
@@ -1694,7 +1681,6 @@ omit =
homeassistant/components/yolink/services.py homeassistant/components/yolink/services.py
homeassistant/components/yolink/siren.py homeassistant/components/yolink/siren.py
homeassistant/components/yolink/switch.py homeassistant/components/yolink/switch.py
homeassistant/components/yolink/valve.py
homeassistant/components/youless/__init__.py homeassistant/components/youless/__init__.py
homeassistant/components/youless/sensor.py homeassistant/components/youless/sensor.py
homeassistant/components/zabbix/* homeassistant/components/zabbix/*
@@ -1703,12 +1689,16 @@ omit =
homeassistant/components/zeroconf/models.py homeassistant/components/zeroconf/models.py
homeassistant/components/zeroconf/usage.py homeassistant/components/zeroconf/usage.py
homeassistant/components/zestimate/sensor.py homeassistant/components/zestimate/sensor.py
homeassistant/components/zeversolar/__init__.py
homeassistant/components/zeversolar/coordinator.py
homeassistant/components/zeversolar/entity.py
homeassistant/components/zeversolar/sensor.py
homeassistant/components/zha/websocket_api.py
homeassistant/components/zha/core/cluster_handlers/* homeassistant/components/zha/core/cluster_handlers/*
homeassistant/components/zha/core/device.py homeassistant/components/zha/core/device.py
homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/gateway.py
homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/helpers.py
homeassistant/components/zha/light.py homeassistant/components/zha/light.py
homeassistant/components/zha/websocket_api.py
homeassistant/components/zhong_hong/climate.py homeassistant/components/zhong_hong/climate.py
homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/ziggo_mediabox_xl/media_player.py
homeassistant/components/zoneminder/* homeassistant/components/zoneminder/*
@@ -1725,6 +1715,15 @@ omit =
homeassistant/components/zwave_me/sensor.py homeassistant/components/zwave_me/sensor.py
homeassistant/components/zwave_me/siren.py homeassistant/components/zwave_me/siren.py
homeassistant/components/zwave_me/switch.py 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/helpers.py
homeassistant/components/myuplink/sensor.py
[report] [report]

View File

@@ -4,12 +4,7 @@
"dockerFile": "../Dockerfile.dev", "dockerFile": "../Dockerfile.dev",
"postCreateCommand": "script/setup", "postCreateCommand": "script/setup",
"postStartCommand": "script/bootstrap", "postStartCommand": "script/bootstrap",
"containerEnv": { "containerEnv": { "DEVCONTAINER": "1" },
"PYTHONASYNCIODEBUG": "1"
},
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
// Port 5683 udp is used by Shelly integration // Port 5683 udp is used by Shelly integration
"appPort": ["8123:8123", "5683:5683/udp"], "appPort": ["8123:8123", "5683:5683/udp"],
"runArgs": ["-e", "GIT_EDITOR=code --wait"], "runArgs": ["-e", "GIT_EDITOR=code --wait"],
@@ -22,15 +17,11 @@
"visualstudioexptteam.vscodeintellicode", "visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github", "GitHub.vscode-pull-request-github"
"GitHub.copilot"
], ],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": { "settings": {
"python.experiments.optOutFrom": ["pythonTestAdapter"], "python.pythonPath": "/usr/local/bin/python",
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"], "python.testing.pytestArgs": ["--no-cov"],
"editor.formatOnPaste": false, "editor.formatOnPaste": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,

View File

@@ -1,14 +0,0 @@
# Black
4de97abc3aa83188666336ce0a015a5bab75bc8f
# Switch formatting from black to ruff-format (#102893)
706add4a57120a93d7b7fe40e722b00d634c76c2
# Prettify json (component test fixtures) (#68892)
053c4428a933c3c04c22642f93c93fccba3e8bfd
# Prettify json (tests) (#68888)
496d90bf00429d9d924caeb0155edc0bf54e86b9
# Bump ruff to 0.3.4 (#112690)
6bb4e7d62c60389608acf4a7d7dacd8f029307dd

View File

@@ -12,8 +12,6 @@ env:
BUILD_TYPE: core BUILD_TYPE: core
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
PIP_TIMEOUT: 60 PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
jobs: jobs:
init: init:
@@ -27,12 +25,12 @@ jobs:
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -51,29 +49,41 @@ jobs:
with: with:
ignore-dev: true ignore-dev: true
- name: Fail if translations files are checked in build_python:
run: | name: Build PyPi package
if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then environment: ${{ needs.init.outputs.channel }}
echo "Translations files are checked in, please remove the following files:" needs: ["init", "build_base"]
find homeassistant/components/*/translations -type f runs-on: ubuntu-latest
exit 1 if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
fi steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download Translations - name: Download Translations
run: python3 -m script.translations download run: python3 -m script.translations download
env: env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Archive translations - name: Build package
shell: bash shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - run: |
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install twine build
python -m build
- name: Upload translations - name: Upload package
uses: actions/upload-artifact@v4.3.3 shell: bash
with: run: |
name: translations export TWINE_USERNAME="__token__"
path: translations.tar.gz export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
if-no-files-found: error
twine upload dist/* --skip-existing
build_base: build_base:
name: Build ${{ matrix.arch }} base core image name: Build ${{ matrix.arch }} base core image
@@ -85,16 +95,15 @@ jobs:
packages: write packages: write
id-token: write id-token: write
strategy: strategy:
fail-fast: false
matrix: matrix:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v3.1.4 uses: dawidd6/action-download-artifact@v3.1.2
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend repo: home-assistant/frontend
@@ -105,7 +114,7 @@ jobs:
- name: Download nightly wheels of intents - name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v3.1.4 uses: dawidd6/action-download-artifact@v3.1.2
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package repo: home-assistant/intents-package
@@ -116,20 +125,17 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Adjust nightly version - name: Adjust nightly version
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
shell: bash shell: bash
env:
UV_PRERELEASE: allow
run: | run: |
python3 -m pip install "$(grep '^uv' < requirements_test.txt)" python3 -m pip install packaging tomli
uv pip install packaging tomli python3 -m pip install .
uv pip install . version="$(python3 script/version_bump.py nightly)"
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}"
@@ -141,7 +147,7 @@ jobs:
sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \ sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \
homeassistant/package_constraints.txt homeassistant/package_constraints.txt
sed -i "s|home-assistant-frontend==.*||" requirements_all.txt python -m script.gen_requirements_all
fi fi
if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then
@@ -159,7 +165,7 @@ jobs:
sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \ sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \
homeassistant/package_constraints.txt homeassistant/package_constraints.txt
sed -i "s|home-assistant-intents==.*||" requirements_all.txt python -m script.gen_requirements_all
fi fi
- name: Adjustments for armhf - name: Adjustments for armhf
@@ -174,15 +180,19 @@ jobs:
sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations - name: Adjustments for 64-bit
uses: actions/download-artifact@v4.1.7 if: matrix.arch == 'amd64' || matrix.arch == 'aarch64'
with:
name: translations
- name: Extract translations
run: | run: |
tar xvf translations.tar.gz # Some speedups are only available on 64-bit, and since
rm translations.tar.gz # 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:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Write meta info file - name: Write meta info file
shell: bash shell: bash
@@ -190,14 +200,14 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.2.0 uses: docker/login-action@v3.0.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2024.03.5 uses: home-assistant/builder@2024.01.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@@ -206,6 +216,17 @@ jobs:
--target /data \ --target /data \
--generic ${{ needs.init.outputs.version }} --generic ${{ needs.init.outputs.version }}
- name: Archive translations
shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@v3
with:
name: translations
path: translations.tar.gz
if-no-files-found: error
build_machine: build_machine:
name: Build ${{ matrix.machine }} machine core image name: Build ${{ matrix.machine }} machine core image
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
@@ -242,7 +263,7 @@ jobs:
- green - green
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set build additional args - name: Set build additional args
run: | run: |
@@ -256,14 +277,14 @@ jobs:
fi fi
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.2.0 uses: docker/login-action@v3.0.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2024.03.5 uses: home-assistant/builder@2024.01.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@@ -279,7 +300,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@@ -315,28 +336,23 @@ jobs:
contents: read contents: read
packages: write packages: write
id-token: write id-token: write
strategy:
matrix:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.5.0 uses: sigstore/cosign-installer@v3.4.0
with: with:
cosign-release: "v2.2.3" cosign-release: "v2.0.2"
- name: Login to DockerHub - name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant' uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.2.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant' uses: docker/login-action@v3.0.0
uses: docker/login-action@v3.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -350,37 +366,41 @@ jobs:
function create_manifest() { function create_manifest() {
local tag_l=${1} local tag_l=${1}
local tag_r=${2} local tag_r=${2}
local registry=${{ matrix.registry }}
docker manifest create "${registry}/home-assistant:${tag_l}" \ for registry in "ghcr.io/home-assistant" "docker.io/homeassistant"
"${registry}/amd64-homeassistant:${tag_r}" \ do
"${registry}/i386-homeassistant:${tag_r}" \
"${registry}/armhf-homeassistant:${tag_r}" \
"${registry}/armv7-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest create "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \ "${registry}/amd64-homeassistant:${tag_r}" \
--os linux --arch amd64 "${registry}/i386-homeassistant:${tag_r}" \
"${registry}/armhf-homeassistant:${tag_r}" \
"${registry}/armv7-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/i386-homeassistant:${tag_r}" \ "${registry}/amd64-homeassistant:${tag_r}" \
--os linux --arch 386 --os linux --arch amd64
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armhf-homeassistant:${tag_r}" \ "${registry}/i386-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v6 --os linux --arch 386
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armv7-homeassistant:${tag_r}" \ "${registry}/armhf-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v7 --os linux --arch arm --variant=v6
docker manifest annotate "${registry}/home-assistant:${tag_l}" \ docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/aarch64-homeassistant:${tag_r}" \ "${registry}/armv7-homeassistant:${tag_r}" \
--os linux --arch arm64 --variant=v8 --os linux --arch arm --variant=v7
docker manifest push --purge "${registry}/home-assistant:${tag_l}" docker manifest annotate "${registry}/home-assistant:${tag_l}" \
cosign sign --yes "${registry}/home-assistant:${tag_l}" "${registry}/aarch64-homeassistant:${tag_r}" \
--os linux --arch arm64 --variant=v8
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
cosign sign --yes "${registry}/home-assistant:${tag_l}"
done
} }
function validate_image() { function validate_image() {
@@ -413,14 +433,12 @@ jobs:
validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}" validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}" validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then # Upload images to dockerhub
# Upload images to dockerhub push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}" push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
fi
# Create version tag # Create version tag
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
@@ -441,44 +459,3 @@ jobs:
v="${{ needs.init.outputs.version }}" v="${{ needs.init.outputs.version }}"
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}" create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
fi fi
build_python:
name: Build PyPi package
environment: ${{ needs.init.outputs.channel }}
needs: ["init", "build_base"]
runs-on: ubuntu-latest
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.1.7
with:
name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Build package
shell: bash
run: |
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install twine build
python -m build
- name: Upload package
shell: bash
run: |
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
twine upload dist/* --skip-existing

View File

@@ -33,10 +33,10 @@ on:
type: boolean type: boolean
env: env:
CACHE_VERSION: 8 CACHE_VERSION: 5
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8 MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.6" HA_SHORT_VERSION: "2024.4"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']" ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@@ -89,18 +89,14 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
id: generate_python_cache_key id: generate_python_cache_key
run: | run: >-
# Include HA_SHORT_VERSION to force the immediate creation echo "key=venv-${{ env.CACHE_VERSION }}-${{
# of a new uv cache entry after a version bump. hashFiles('requirements_test.txt') }}-${{
echo "key=venv-${{ env.CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-${{
hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_all.txt') }}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT
hashFiles('script/gen_requirements_all.py') }}" >> $GITHUB_OUTPUT
- name: Generate partial pre-commit restore key - name: Generate partial pre-commit restore key
id: generate_pre-commit_cache_key id: generate_pre-commit_cache_key
run: >- run: >-
@@ -226,16 +222,16 @@ jobs:
- info - info
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.0.2 uses: actions/cache@v4.0.1
with: with:
path: venv path: venv
key: >- key: >-
@@ -247,11 +243,11 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install "$(grep '^uv' < requirements_test.txt)" pip install "$(cat requirements_test.txt | grep uv)"
uv pip install "$(cat requirements_test.txt | grep pre-commit)" uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v4.0.2 uses: actions/cache@v4.0.1
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true lookup-only: true
@@ -272,16 +268,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -290,7 +286,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -312,16 +308,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -330,7 +326,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -351,16 +347,16 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -369,7 +365,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }} needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -445,23 +441,21 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Generate partial uv restore key - name: Generate partial uv restore key
id: generate-uv-key id: generate-uv-key
run: | run: >-
uv_version=$(cat requirements_test.txt | grep uv | cut -d '=' -f 3) echo "key=uv-${{ env.UV_CACHE_VERSION }}-${{
echo "version=${uv_version}" >> $GITHUB_OUTPUT
echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.0.2 uses: actions/cache@v4.0.1
with: with:
path: venv path: venv
lookup-only: true lookup-only: true
@@ -470,20 +464,17 @@ jobs:
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache - name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.0.2 uses: actions/cache@v4.0.1
with: with:
path: ${{ env.UV_CACHE_DIR }} path: ${{ env.UV_CACHE }}
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-uv-key.outputs.key }} steps.generate-uv-key.outputs.key }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ env.UV_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Install additional OS dependencies - name: Install additional OS dependencies
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update sudo apt-get update
sudo apt-get -y install \ sudo apt-get -y install \
bluez \ bluez \
@@ -493,7 +484,6 @@ jobs:
libavfilter-dev \ libavfilter-dev \
libavformat-dev \ libavformat-dev \
libavutil-dev \ libavutil-dev \
libgammu-dev \
libswresample-dev \ libswresample-dev \
libswscale-dev \ libswscale-dev \
libudev-dev libudev-dev
@@ -503,11 +493,9 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version python --version
pip install "$(grep '^uv' < requirements_test.txt)" pip install "$(cat requirements_test.txt | grep uv)"
uv pip install -U "pip>=21.3.1" setuptools wheel uv pip install -U "pip>=21.3.1" setuptools wheel
uv pip install -r requirements.txt uv pip install -r requirements_all.txt
python -m script.gen_requirements_all ci
uv pip install -r requirements_all_pytest.txt
uv pip install -r requirements_test.txt uv pip install -r requirements_test.txt
uv pip install -e . --config-settings editable_mode=compat uv pip install -e . --config-settings editable_mode=compat
@@ -522,16 +510,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -554,16 +542,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -587,16 +575,16 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -611,14 +599,14 @@ jobs:
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
pylint --ignore-missing-annotations=y homeassistant pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant
- name: Run pylint (partially) - name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
shell: bash shell: bash
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }} pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
mypy: mypy:
name: Check mypy name: Check mypy
@@ -631,10 +619,10 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@@ -647,7 +635,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -655,7 +643,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Restore mypy cache - name: Restore mypy cache
uses: actions/cache@v4.0.2 uses: actions/cache@v4.0.1
with: with:
path: .mypy_cache path: .mypy_cache
key: >- key: >-
@@ -682,63 +670,14 @@ jobs:
python --version python --version
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
prepare-pytest-full: pytest:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: | if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true' && github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true' && github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
&& needs.info.outputs.test_full_suite == 'true' && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob)
needs:
- info
- base
name: Split tests for full run
steps:
- name: Install additional OS dependencies
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update
sudo apt-get -y install \
bluez \
ffmpeg \
libgammu-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.0.2
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run split_tests.py
run: |
. venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets
uses: actions/upload-artifact@v4.3.3
with:
name: pytest_buckets
path: pytest_buckets.txt
overwrite: true
pytest-full:
runs-on: ubuntu-22.04
if: |
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& needs.info.outputs.test_full_suite == 'true'
needs: needs:
- info - info
- base - base
@@ -747,7 +686,6 @@ jobs:
- lint-other - lint-other
- lint-ruff - lint-ruff
- mypy - mypy
- prepare-pytest-full
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -758,23 +696,21 @@ jobs:
steps: steps:
- name: Install additional OS dependencies - name: Install additional OS dependencies
run: | run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update sudo apt-get update
sudo apt-get -y install \ sudo apt-get -y install \
bluez \ bluez \
ffmpeg \ ffmpeg
libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -786,15 +722,12 @@ jobs:
- name: Register pytest slow test problem matcher - name: Register pytest slow test problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.1.7
with:
name: pytest_buckets
- name: Compile English translations - name: Compile English translations
run: | run: |
. venv/bin/activate . venv/bin/activate
python3 -m script.translations develop --all python3 -m script.translations develop --all
- name: Run pytest - name: Run pytest (fully)
if: needs.info.outputs.test_full_suite == 'true'
timeout-minutes: 60 timeout-minutes: 60
id: pytest-full id: pytest-full
env: env:
@@ -815,27 +748,62 @@ jobs:
--durations=10 \ --durations=10 \
-n auto \ -n auto \
--dist=loadfile \ --dist=loadfile \
--test-group-count ${{ needs.info.outputs.test_group_count }} \
--test-group=${{ matrix.group }} \
${cov_params[@]} \ ${cov_params[@]} \
-o console_output_style=count \ -o console_output_style=count \
-p no:sugar \ -p no:sugar \
$(sed -n "${{ matrix.group }},1p" pytest_buckets.txt) \ tests \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Run pytest (partially)
if: needs.info.outputs.test_full_suite == 'false'
timeout-minutes: 10
id: pytest-partial
shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: |
. venv/bin/activate
python --version
set -o pipefail
if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then
echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py"
exit 1
fi
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.${{ matrix.group }}")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -b -X dev -m pytest \
-qq \
--timeout=9 \
-n auto \
${cov_params[@]} \
-o console_output_style=count \
--durations=0 \
--durations-min=1 \
-p no:sugar \
tests/components/${{ matrix.group }} \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure' if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure')
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt path: pytest-*.txt
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }} name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml path: coverage.xml
overwrite: true overwrite: true
- name: Remove pytest_buckets
run: rm pytest_buckets.txt
- name: Check dirty - name: Check dirty
run: | run: |
./script/check_dirty ./script/check_dirty
@@ -874,23 +842,22 @@ jobs:
steps: steps:
- name: Install additional OS dependencies - name: Install additional OS dependencies
run: | run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update sudo apt-get update
sudo apt-get -y install \ sudo apt-get -y install \
bluez \ bluez \
ffmpeg \ ffmpeg \
libmariadb-dev-compat libmariadb-dev-compat
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -945,7 +912,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@@ -953,7 +920,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }} steps.pytest-partial.outputs.mariadb }}
@@ -997,23 +964,22 @@ jobs:
steps: steps:
- name: Install additional OS dependencies - name: Install additional OS dependencies
run: | run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update sudo apt-get update
sudo apt-get -y install \ sudo apt-get -y install \
bluez \ bluez \
ffmpeg \ ffmpeg \
postgresql-server-dev-14 postgresql-server-dev-14
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@v4.0.2 uses: actions/cache/restore@v4.0.1
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -1069,7 +1035,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output - name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure' if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@@ -1077,7 +1043,7 @@ jobs:
overwrite: true overwrite: true
- name: Upload coverage artifact - name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: coverage-${{ matrix.python-version }}-${{ name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }} steps.pytest-partial.outputs.postgresql }}
@@ -1087,160 +1053,39 @@ jobs:
run: | run: |
./script/check_dirty ./script/check_dirty
coverage-full: coverage:
name: Upload test coverage to Codecov (full suite) name: Upload test coverage to Codecov
if: needs.info.outputs.skip_coverage != 'true' if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: needs:
- info - info
- pytest-full - pytest
- pytest-postgres
- pytest-mariadb
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.4
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov (full coverage)
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v4.4.1 uses: Wandalen/wretry.action@v1.4.10
with: with:
fail_ci_if_error: true action: codecov/codecov-action@v3.1.3
flags: full-suite with: |
token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true
flags: full-suite
pytest-partial: token: ${{ env.CODECOV_TOKEN }}
runs-on: ubuntu-22.04 attempt_limit: 5
if: | attempt_delay: 30000
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') - name: Upload coverage to Codecov (partial coverage)
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& needs.info.outputs.tests_glob
&& needs.info.outputs.test_full_suite == 'false'
needs:
- info
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- mypy
strategy:
fail-fast: false
matrix:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
name: >-
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
steps:
- name: Install additional OS dependencies
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update
sudo apt-get -y install \
bluez \
ffmpeg \
libgammu-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.1.0
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.2
with:
path: venv
fail-on-cache-miss: true
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/python.json"
- name: Register pytest slow test problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Compile English translations
run: |
. venv/bin/activate
python3 -m script.translations develop --all
- name: Run pytest
timeout-minutes: 10
id: pytest-partial
shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: |
. venv/bin/activate
python --version
set -o pipefail
if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then
echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py"
exit 1
fi
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.${{ matrix.group }}")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -b -X dev -m pytest \
-qq \
--timeout=9 \
-n auto \
${cov_params[@]} \
-o console_output_style=count \
--durations=0 \
--durations-min=1 \
-p no:sugar \
tests/components/${{ matrix.group }} \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.3
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Check dirty
run: |
./script/check_dirty
coverage-partial:
name: Upload test coverage to Codecov (partial suite)
if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-22.04
needs:
- info
- pytest-partial
timeout-minutes: 10
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.7
with:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v4.4.1 uses: Wandalen/wretry.action@v1.4.10
with: with:
fail_ci_if_error: true action: codecov/codecov-action@v3.1.3
token: ${{ secrets.CODECOV_TOKEN }} with: |
fail_ci_if_error: true
token: ${{ env.CODECOV_TOKEN }}
attempt_limit: 5
attempt_delay: 30000

View File

@@ -21,14 +21,14 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.25.6 uses: github/codeql-action/init@v3.24.7
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.25.6 uses: github/codeql-action/analyze@v3.24.7
with: with:
category: "/language:python" category: "/language:python"

View File

@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -14,10 +14,6 @@ on:
- "homeassistant/package_constraints.txt" - "homeassistant/package_constraints.txt"
- "requirements_all.txt" - "requirements_all.txt"
- "requirements.txt" - "requirements.txt"
- "script/gen_requirements_all.py"
env:
DEFAULT_PYTHON: "3.12"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref_name}} group: ${{ github.workflow }}-${{ github.ref_name}}
@@ -32,22 +28,7 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }} architectures: ${{ steps.info.outputs.architectures }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Create Python virtual environment
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install "$(grep '^uv' < requirements_test.txt)"
uv pip install -r requirements.txt
- name: Get information - name: Get information
id: info id: info
@@ -82,30 +63,19 @@ jobs:
) > .env_file ) > .env_file
- name: Upload env_file - name: Upload env_file
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: env_file name: env_file
path: ./.env_file path: ./.env_file
overwrite: true overwrite: true
- name: Upload requirements_diff - name: Upload requirements_diff
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: requirements_diff name: requirements_diff
path: ./requirements_diff.txt path: ./requirements_diff.txt
overwrite: true overwrite: true
- name: Generate requirements
run: |
. venv/bin/activate
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.3.3
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
core: core:
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2) name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
@@ -118,15 +88,15 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.4
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.4
with: with:
name: requirements_diff name: requirements_diff
@@ -156,30 +126,57 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.4
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v4.1.4
with: with:
name: requirements_diff name: requirements_diff
- name: Download requirements_all_wheels - name: (Un)comment packages
uses: actions/download-artifact@v4.1.7 run: |
with: requirement_files="requirements_all.txt requirements_diff.txt"
name: requirements_all_wheels for requirement_file in ${requirement_files}; do
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
sed -i "s|# evdev|evdev|g" ${requirement_file}
sed -i "s|# pycups|pycups|g" ${requirement_file}
sed -i "s|# homekit|homekit|g" ${requirement_file}
sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file}
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
# Some packages are not buildable on armhf anymore
if [ "${{ matrix.arch }}" = "armhf" ]; then
# Pandas has issues building on armhf, it is expected they
# will drop the platform in the near future (they consider it
# "flimsy" on 386). The following packages depend on pandas,
# so we comment them out.
sed -i "s|env-canada|# env-canada|g" ${requirement_file}
sed -i "s|noaa-coops|# noaa-coops|g" ${requirement_file}
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 - name: Split requirements all
run: | run: |
# We split requirements all into multiple files. # We split requirements all into two different files.
# This is to prevent the build from running out of memory when # This is to prevent the build from running out of memory when
# resolving packages on 32-bits systems (like armhf, armv7). # resolving packages on 32-bits systems (like armhf, armv7).
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all_wheels_${{ matrix.arch }}.txt requirements_all.txt split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all.txt requirements_all.txt
- name: Create requirements for cython<3 - name: Create requirements for cython<3
run: | run: |
@@ -211,7 +208,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true 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" 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;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt" requirements: "requirements_old-cython.txt"
@@ -226,7 +223,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true 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;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa" requirements: "requirements_all.txtaa"
@@ -240,7 +237,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true 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;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab" requirements: "requirements_all.txtab"
@@ -254,7 +251,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true 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;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac" requirements: "requirements_all.txtac"

4
.gitignore vendored
View File

@@ -34,7 +34,6 @@ Icon
# GITHUB Proposed Python stuff: # GITHUB Proposed Python stuff:
*.py[cod] *.py[cod]
__pycache__
# C extensions # C extensions
*.so *.so
@@ -133,6 +132,3 @@ tmp_cache
# python-language-server / Rope # python-language-server / Rope
.ropeproject .ropeproject
# Will be created from script/split_tests.py
pytest_buckets.txt

View File

@@ -1,21 +1,21 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.6 rev: v0.2.1
hooks: hooks:
- id: ruff - id: ruff
args: args:
- --fix - --fix
- id: ruff-format - id: ruff-format
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$ files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.3.0 rev: v2.2.2
hooks: hooks:
- id: codespell - id: codespell
args: args:
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,pres,ser,ue - --ignore-words-list=additionals,alle,alot,bund,currenty,datas,farenheit,falsy,fo,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,withing,zar
- --skip="./.*,*.csv,*.json,*.ambr" - --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2 - --quiet-level=2
exclude_types: [csv, json, html] exclude_types: [csv, json]
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
@@ -30,7 +30,7 @@ repos:
- --branch=master - --branch=master
- --branch=rc - --branch=rc
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.35.1 rev: v1.32.0
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
@@ -61,15 +61,15 @@ repos:
name: mypy name: mypy
entry: script/run-in-env.sh mypy entry: script/run-in-env.sh mypy
language: script language: script
types_or: [python, pyi] types: [python]
require_serial: true require_serial: true
files: ^(homeassistant|pylint)/.+\.(py|pyi)$ files: ^(homeassistant|pylint)/.+\.py$
- id: pylint - id: pylint
name: pylint name: pylint
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
language: script language: script
types_or: [python, pyi] types: [python]
files: ^homeassistant/.+\.(py|pyi)$ files: ^homeassistant/.+\.py$
- id: gen_requirements_all - id: gen_requirements_all
name: gen_requirements_all name: gen_requirements_all
entry: script/run-in-env.sh python3 -m script.gen_requirements_all entry: script/run-in-env.sh python3 -m script.gen_requirements_all
@@ -83,7 +83,7 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements_test.txt)$ files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$
- id: hassfest-metadata - id: hassfest-metadata
name: hassfest-metadata name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata

View File

@@ -48,7 +48,6 @@ homeassistant.components.adax.*
homeassistant.components.adguard.* homeassistant.components.adguard.*
homeassistant.components.aftership.* homeassistant.components.aftership.*
homeassistant.components.air_quality.* homeassistant.components.air_quality.*
homeassistant.components.airgradient.*
homeassistant.components.airly.* homeassistant.components.airly.*
homeassistant.components.airnow.* homeassistant.components.airnow.*
homeassistant.components.airq.* homeassistant.components.airq.*
@@ -66,7 +65,7 @@ homeassistant.components.alexa.*
homeassistant.components.alpha_vantage.* homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.* homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.* homeassistant.components.ambiclimate.*
homeassistant.components.ambient_station.* homeassistant.components.ambient_station.*
homeassistant.components.amcrest.* homeassistant.components.amcrest.*
homeassistant.components.ampio.* homeassistant.components.ampio.*
@@ -84,7 +83,6 @@ homeassistant.components.api.*
homeassistant.components.apple_tv.* homeassistant.components.apple_tv.*
homeassistant.components.apprise.* homeassistant.components.apprise.*
homeassistant.components.aprs.* homeassistant.components.aprs.*
homeassistant.components.apsystems.*
homeassistant.components.aqualogic.* homeassistant.components.aqualogic.*
homeassistant.components.aquostv.* homeassistant.components.aquostv.*
homeassistant.components.aranet.* homeassistant.components.aranet.*
@@ -168,12 +166,10 @@ homeassistant.components.electric_kiwi.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.elkm1.* homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.* homeassistant.components.emulated_hue.*
homeassistant.components.energenie_power_sockets.*
homeassistant.components.energy.* homeassistant.components.energy.*
homeassistant.components.energyzero.* homeassistant.components.energyzero.*
homeassistant.components.enigma2.* homeassistant.components.enigma2.*
homeassistant.components.enphase_envoy.* homeassistant.components.enphase_envoy.*
homeassistant.components.eq3btsmart.*
homeassistant.components.esphome.* homeassistant.components.esphome.*
homeassistant.components.event.* homeassistant.components.event.*
homeassistant.components.evil_genius_labs.* homeassistant.components.evil_genius_labs.*
@@ -236,7 +232,6 @@ homeassistant.components.homeworks.*
homeassistant.components.http.* homeassistant.components.http.*
homeassistant.components.huawei_lte.* homeassistant.components.huawei_lte.*
homeassistant.components.humidifier.* homeassistant.components.humidifier.*
homeassistant.components.husqvarna_automower.*
homeassistant.components.hydrawise.* homeassistant.components.hydrawise.*
homeassistant.components.hyperion.* homeassistant.components.hyperion.*
homeassistant.components.ibeacon.* homeassistant.components.ibeacon.*
@@ -245,7 +240,6 @@ homeassistant.components.image.*
homeassistant.components.image_processing.* homeassistant.components.image_processing.*
homeassistant.components.image_upload.* homeassistant.components.image_upload.*
homeassistant.components.imap.* homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.input_button.* homeassistant.components.input_button.*
homeassistant.components.input_select.* homeassistant.components.input_select.*
homeassistant.components.input_text.* homeassistant.components.input_text.*
@@ -302,7 +296,6 @@ homeassistant.components.minecraft_server.*
homeassistant.components.mjpeg.* homeassistant.components.mjpeg.*
homeassistant.components.modbus.* homeassistant.components.modbus.*
homeassistant.components.modem_callerid.* homeassistant.components.modem_callerid.*
homeassistant.components.monzo.*
homeassistant.components.moon.* homeassistant.components.moon.*
homeassistant.components.mopeka.* homeassistant.components.mopeka.*
homeassistant.components.motionmount.* homeassistant.components.motionmount.*
@@ -341,6 +334,7 @@ homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.* homeassistant.components.pi_hole.*
homeassistant.components.ping.* homeassistant.components.ping.*
homeassistant.components.plugwise.* homeassistant.components.plugwise.*
homeassistant.components.poolsense.*
homeassistant.components.powerwall.* homeassistant.components.powerwall.*
homeassistant.components.private_ble_device.* homeassistant.components.private_ble_device.*
homeassistant.components.prometheus.* homeassistant.components.prometheus.*
@@ -367,7 +361,6 @@ homeassistant.components.rest_command.*
homeassistant.components.rfxtrx.* homeassistant.components.rfxtrx.*
homeassistant.components.rhasspy.* homeassistant.components.rhasspy.*
homeassistant.components.ridwell.* homeassistant.components.ridwell.*
homeassistant.components.ring.*
homeassistant.components.rituals_perfume_genie.* homeassistant.components.rituals_perfume_genie.*
homeassistant.components.roku.* homeassistant.components.roku.*
homeassistant.components.romy.* homeassistant.components.romy.*
@@ -428,7 +421,6 @@ homeassistant.components.tcp.*
homeassistant.components.technove.* homeassistant.components.technove.*
homeassistant.components.tedee.* homeassistant.components.tedee.*
homeassistant.components.text.* homeassistant.components.text.*
homeassistant.components.thethingsnetwork.*
homeassistant.components.threshold.* homeassistant.components.threshold.*
homeassistant.components.tibber.* homeassistant.components.tibber.*
homeassistant.components.tile.* homeassistant.components.tile.*

4
.vscode/tasks.json vendored
View File

@@ -103,7 +103,7 @@
{ {
"label": "Install all Requirements", "label": "Install all Requirements",
"type": "shell", "type": "shell",
"command": "uv pip install -r requirements_all.txt", "command": "pip3 install -r requirements_all.txt",
"group": { "group": {
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
@@ -117,7 +117,7 @@
{ {
"label": "Install all Test Requirements", "label": "Install all Test Requirements",
"type": "shell", "type": "shell",
"command": "uv pip install -r requirements_test_all.txt", "command": "pip3 install -r requirements_test_all.txt",
"group": { "group": {
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true

View File

@@ -5,30 +5,13 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Home Assistant Core # Home Assistant Core
.core_files.yaml @home-assistant/core setup.cfg @home-assistant/core
.git-blame-ignore-revs @home-assistant/core
.gitattributes @home-assistant/core
.gitignore @home-assistant/core
.hadolint.yaml @home-assistant/core
.pre-commit-config.yaml @home-assistant/core
.prettierignore @home-assistant/core
.yamllint @home-assistant/core
pyproject.toml @home-assistant/core pyproject.toml @home-assistant/core
requirements_test.txt @home-assistant/core
/.devcontainer/ @home-assistant/core
/.github/ @home-assistant/core
/.vscode/ @home-assistant/core
/homeassistant/*.py @home-assistant/core /homeassistant/*.py @home-assistant/core
/homeassistant/auth/ @home-assistant/core
/homeassistant/backports/ @home-assistant/core
/homeassistant/helpers/ @home-assistant/core /homeassistant/helpers/ @home-assistant/core
/homeassistant/scripts/ @home-assistant/core
/homeassistant/util/ @home-assistant/core /homeassistant/util/ @home-assistant/core
/pylint/ @home-assistant/core
/script/ @home-assistant/core
# Home Assistant Supervisor # Home Assistant Supervisor
.dockerignore @home-assistant/supervisor
build.json @home-assistant/supervisor build.json @home-assistant/supervisor
/machine/ @home-assistant/supervisor /machine/ @home-assistant/supervisor
/rootfs/ @home-assistant/supervisor /rootfs/ @home-assistant/supervisor
@@ -56,8 +39,6 @@ build.json @home-assistant/supervisor
/tests/components/agent_dvr/ @ispysoftware /tests/components/agent_dvr/ @ispysoftware
/homeassistant/components/air_quality/ @home-assistant/core /homeassistant/components/air_quality/ @home-assistant/core
/tests/components/air_quality/ @home-assistant/core /tests/components/air_quality/ @home-assistant/core
/homeassistant/components/airgradient/ @airgradienthq @joostlek
/tests/components/airgradient/ @airgradienthq @joostlek
/homeassistant/components/airly/ @bieniu /homeassistant/components/airly/ @bieniu
/tests/components/airly/ @bieniu /tests/components/airly/ @bieniu
/homeassistant/components/airnow/ @asymworks /homeassistant/components/airnow/ @asymworks
@@ -80,8 +61,8 @@ build.json @home-assistant/supervisor
/tests/components/airzone/ @Noltari /tests/components/airzone/ @Noltari
/homeassistant/components/airzone_cloud/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari
/tests/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari
/homeassistant/components/aladdin_connect/ @swcloudgenie /homeassistant/components/aladdin_connect/ @mkmer
/tests/components/aladdin_connect/ @swcloudgenie /tests/components/aladdin_connect/ @mkmer
/homeassistant/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alarm_control_panel/ @home-assistant/core
/tests/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core
/homeassistant/components/alert/ @home-assistant/core @frenck /homeassistant/components/alert/ @home-assistant/core @frenck
@@ -90,8 +71,8 @@ build.json @home-assistant/supervisor
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/amberelectric/ @madpilot /homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot
/homeassistant/components/ambient_network/ @thomaskistler /homeassistant/components/ambiclimate/ @danielhiversen
/tests/components/ambient_network/ @thomaskistler /tests/components/ambiclimate/ @danielhiversen
/homeassistant/components/ambient_station/ @bachya /homeassistant/components/ambient_station/ @bachya
/tests/components/ambient_station/ @bachya /tests/components/ambient_station/ @bachya
/homeassistant/components/amcrest/ @flacjacket /homeassistant/components/amcrest/ @flacjacket
@@ -127,15 +108,11 @@ build.json @home-assistant/supervisor
/tests/components/aprilaire/ @chamberlain2007 /tests/components/aprilaire/ @chamberlain2007
/homeassistant/components/aprs/ @PhilRW /homeassistant/components/aprs/ @PhilRW
/tests/components/aprs/ @PhilRW /tests/components/aprs/ @PhilRW
/homeassistant/components/apsystems/ @mawoka-myblock @SonnenladenGmbH /homeassistant/components/aranet/ @aschmitz @thecode
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH /tests/components/aranet/ @aschmitz @thecode
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
/tests/components/aranet/ @aschmitz @thecode @anrijs
/homeassistant/components/arcam_fmj/ @elupus /homeassistant/components/arcam_fmj/ @elupus
/tests/components/arcam_fmj/ @elupus /tests/components/arcam_fmj/ @elupus
/homeassistant/components/arris_tg2492lg/ @vanbalken /homeassistant/components/arris_tg2492lg/ @vanbalken
/homeassistant/components/arve/ @ikalnyi
/tests/components/arve/ @ikalnyi
/homeassistant/components/aseko_pool_live/ @milanmeu /homeassistant/components/aseko_pool_live/ @milanmeu
/tests/components/aseko_pool_live/ @milanmeu /tests/components/aseko_pool_live/ @milanmeu
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam /homeassistant/components/assist_pipeline/ @balloob @synesthesiam
@@ -163,8 +140,6 @@ build.json @home-assistant/supervisor
/tests/components/awair/ @ahayworth @danielsjf /tests/components/awair/ @ahayworth @danielsjf
/homeassistant/components/axis/ @Kane610 /homeassistant/components/axis/ @Kane610
/tests/components/axis/ @Kane610 /tests/components/axis/ @Kane610
/homeassistant/components/azure_data_explorer/ @kaareseras
/tests/components/azure_data_explorer/ @kaareseras
/homeassistant/components/azure_devops/ @timmo001 /homeassistant/components/azure_devops/ @timmo001
/tests/components/azure_devops/ @timmo001 /tests/components/azure_devops/ @timmo001
/homeassistant/components/azure_event_hub/ @eavanvalkenburg /homeassistant/components/azure_event_hub/ @eavanvalkenburg
@@ -324,8 +299,8 @@ build.json @home-assistant/supervisor
/tests/components/discovergy/ @jpbede /tests/components/discovergy/ @jpbede
/homeassistant/components/dlink/ @tkdrob /homeassistant/components/dlink/ @tkdrob
/tests/components/dlink/ @tkdrob /tests/components/dlink/ @tkdrob
/homeassistant/components/dlna_dmr/ @chishm /homeassistant/components/dlna_dmr/ @StevenLooman @chishm
/tests/components/dlna_dmr/ @chishm /tests/components/dlna_dmr/ @StevenLooman @chishm
/homeassistant/components/dlna_dms/ @chishm /homeassistant/components/dlna_dms/ @chishm
/tests/components/dlna_dms/ @chishm /tests/components/dlna_dms/ @chishm
/homeassistant/components/dnsip/ @gjohansson-ST /homeassistant/components/dnsip/ @gjohansson-ST
@@ -334,16 +309,14 @@ build.json @home-assistant/supervisor
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket /tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
/homeassistant/components/dormakaba_dkey/ @emontnemery /homeassistant/components/dormakaba_dkey/ @emontnemery
/tests/components/dormakaba_dkey/ @emontnemery /tests/components/dormakaba_dkey/ @emontnemery
/homeassistant/components/downloader/ @erwindouna
/tests/components/downloader/ @erwindouna
/homeassistant/components/dremel_3d_printer/ @tkdrob /homeassistant/components/dremel_3d_printer/ @tkdrob
/tests/components/dremel_3d_printer/ @tkdrob /tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer /homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer /tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dsmr/ @Robbie1221 @frenck /homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna /homeassistant/components/dsmr_reader/ @sorted-bits @glodenox
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna /tests/components/dsmr_reader/ @sorted-bits @glodenox
/homeassistant/components/duotecno/ @cereal2nd /homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
@@ -386,14 +359,11 @@ build.json @home-assistant/supervisor
/tests/components/emulated_hue/ @bdraco @Tho85 /tests/components/emulated_hue/ @bdraco @Tho85
/homeassistant/components/emulated_kasa/ @kbickar /homeassistant/components/emulated_kasa/ @kbickar
/tests/components/emulated_kasa/ @kbickar /tests/components/emulated_kasa/ @kbickar
/homeassistant/components/energenie_power_sockets/ @gnumpi
/tests/components/energenie_power_sockets/ @gnumpi
/homeassistant/components/energy/ @home-assistant/core /homeassistant/components/energy/ @home-assistant/core
/tests/components/energy/ @home-assistant/core /tests/components/energy/ @home-assistant/core
/homeassistant/components/energyzero/ @klaasnicolaas /homeassistant/components/energyzero/ @klaasnicolaas
/tests/components/energyzero/ @klaasnicolaas /tests/components/energyzero/ @klaasnicolaas
/homeassistant/components/enigma2/ @autinerd /homeassistant/components/enigma2/ @autinerd
/tests/components/enigma2/ @autinerd
/homeassistant/components/enocean/ @bdurrer /homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
@@ -402,14 +372,11 @@ build.json @home-assistant/supervisor
/homeassistant/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50 /homeassistant/components/ephember/ @ttroy50
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
/tests/components/epic_games_store/ @hacf-fr @Quentame
/homeassistant/components/epion/ @lhgravendeel /homeassistant/components/epion/ @lhgravendeel
/tests/components/epion/ @lhgravendeel /tests/components/epion/ @lhgravendeel
/homeassistant/components/epson/ @pszafer /homeassistant/components/epson/ @pszafer
/tests/components/epson/ @pszafer /tests/components/epson/ @pszafer
/homeassistant/components/eq3btsmart/ @eulemitkeule @dbuezas /homeassistant/components/epsonworkforce/ @ThaStealth
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
/homeassistant/components/escea/ @lazdavila /homeassistant/components/escea/ @lazdavila
/tests/components/escea/ @lazdavila /tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco /homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
@@ -466,8 +433,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/forked_daapd/ @uvjustin /homeassistant/components/forked_daapd/ @uvjustin
/tests/components/forked_daapd/ @uvjustin /tests/components/forked_daapd/ @uvjustin
/homeassistant/components/fortios/ @kimfrellsen /homeassistant/components/fortios/ @kimfrellsen
/homeassistant/components/foscam/ @krmarien /homeassistant/components/foscam/ @skgsergio @krmarien
/tests/components/foscam/ @krmarien /tests/components/foscam/ @skgsergio @krmarien
/homeassistant/components/freebox/ @hacf-fr @Quentame /homeassistant/components/freebox/ @hacf-fr @Quentame
/tests/components/freebox/ @hacf-fr @Quentame /tests/components/freebox/ @hacf-fr @Quentame
/homeassistant/components/freedompro/ @stefano055415 /homeassistant/components/freedompro/ @stefano055415
@@ -486,8 +453,6 @@ build.json @home-assistant/supervisor
/tests/components/frontier_silicon/ @wlcrs /tests/components/frontier_silicon/ @wlcrs
/homeassistant/components/fully_kiosk/ @cgarwood /homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fyta/ @dontinelli
/tests/components/fyta/ @dontinelli
/homeassistant/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus /homeassistant/components/gardena_bluetooth/ @elupus
@@ -554,14 +519,14 @@ build.json @home-assistant/supervisor
/tests/components/group/ @home-assistant/core /tests/components/group/ @home-assistant/core
/homeassistant/components/guardian/ @bachya /homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya /tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r /homeassistant/components/habitica/ @ASMfreaK @leikoilja
/tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r /tests/components/habitica/ @ASMfreaK @leikoilja
/homeassistant/components/hardkernel/ @home-assistant/core /homeassistant/components/hardkernel/ @home-assistant/core
/tests/components/hardkernel/ @home-assistant/core /tests/components/hardkernel/ @home-assistant/core
/homeassistant/components/hardware/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core
/tests/components/hardware/ @home-assistant/core /tests/components/hardware/ @home-assistant/core
/homeassistant/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan /homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan /tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
/homeassistant/components/hassio/ @home-assistant/supervisor /homeassistant/components/hassio/ @home-assistant/supervisor
/tests/components/hassio/ @home-assistant/supervisor /tests/components/hassio/ @home-assistant/supervisor
/homeassistant/components/hdmi_cec/ @inytar /homeassistant/components/hdmi_cec/ @inytar
@@ -603,10 +568,8 @@ build.json @home-assistant/supervisor
/tests/components/homekit/ @bdraco /tests/components/homekit/ @bdraco
/homeassistant/components/homekit_controller/ @Jc2k @bdraco /homeassistant/components/homekit_controller/ @Jc2k @bdraco
/tests/components/homekit_controller/ @Jc2k @bdraco /tests/components/homekit_controller/ @Jc2k @bdraco
/homeassistant/components/homematic/ @pvizeli /homeassistant/components/homematic/ @pvizeli @danielperna84
/tests/components/homematic/ @pvizeli /tests/components/homematic/ @pvizeli @danielperna84
/homeassistant/components/homematicip_cloud/ @hahn-th
/tests/components/homematicip_cloud/ @hahn-th
/homeassistant/components/homewizard/ @DCSBL /homeassistant/components/homewizard/ @DCSBL
/tests/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer /homeassistant/components/honeywell/ @rdfurman @mkmer
@@ -654,8 +617,6 @@ build.json @home-assistant/supervisor
/tests/components/image_upload/ @home-assistant/core /tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @jbouwh /homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh /tests/components/imap/ @jbouwh
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/improv_ble/ @emontnemery /homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @zxdavb /homeassistant/components/incomfort/ @zxdavb
@@ -696,10 +657,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/iqvia/ @bachya /homeassistant/components/iqvia/ @bachya
/tests/components/iqvia/ @bachya /tests/components/iqvia/ @bachya
/homeassistant/components/irish_rail_transport/ @ttroy50 /homeassistant/components/irish_rail_transport/ @ttroy50
/homeassistant/components/isal/ @bdraco /homeassistant/components/islamic_prayer_times/ @engrbm87
/tests/components/isal/ @bdraco /tests/components/islamic_prayer_times/ @engrbm87
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
/homeassistant/components/iss/ @DurgNomis-drol /homeassistant/components/iss/ @DurgNomis-drol
/tests/components/iss/ @DurgNomis-drol /tests/components/iss/ @DurgNomis-drol
/homeassistant/components/isy994/ @bdraco @shbatm /homeassistant/components/isy994/ @bdraco @shbatm
@@ -768,8 +727,7 @@ build.json @home-assistant/supervisor
/tests/components/leaone/ @bdraco /tests/components/leaone/ @bdraco
/homeassistant/components/led_ble/ @bdraco /homeassistant/components/led_ble/ @bdraco
/tests/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco
/homeassistant/components/lg_netcast/ @Drafteed @splinter98 /homeassistant/components/lg_netcast/ @Drafteed
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob /homeassistant/components/lidarr/ @tkdrob
/tests/components/lidarr/ @tkdrob /tests/components/lidarr/ @tkdrob
/homeassistant/components/light/ @home-assistant/core /homeassistant/components/light/ @home-assistant/core
@@ -873,22 +831,18 @@ build.json @home-assistant/supervisor
/tests/components/moehlenhoff_alpha2/ @j-a-n /tests/components/moehlenhoff_alpha2/ @j-a-n
/homeassistant/components/monoprice/ @etsinko @OnFreund /homeassistant/components/monoprice/ @etsinko @OnFreund
/tests/components/monoprice/ @etsinko @OnFreund /tests/components/monoprice/ @etsinko @OnFreund
/homeassistant/components/monzo/ @jakemartin-icl
/tests/components/monzo/ @jakemartin-icl
/homeassistant/components/moon/ @fabaff @frenck /homeassistant/components/moon/ @fabaff @frenck
/tests/components/moon/ @fabaff @frenck /tests/components/moon/ @fabaff @frenck
/homeassistant/components/mopeka/ @bdraco /homeassistant/components/mopeka/ @bdraco
/tests/components/mopeka/ @bdraco /tests/components/mopeka/ @bdraco
/homeassistant/components/motion_blinds/ @starkillerOG /homeassistant/components/motion_blinds/ @starkillerOG
/tests/components/motion_blinds/ @starkillerOG /tests/components/motion_blinds/ @starkillerOG
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
/tests/components/motionblinds_ble/ @LennP @jerrybboy
/homeassistant/components/motioneye/ @dermotduffy /homeassistant/components/motioneye/ @dermotduffy
/tests/components/motioneye/ @dermotduffy /tests/components/motioneye/ @dermotduffy
/homeassistant/components/motionmount/ @RJPoelstra /homeassistant/components/motionmount/ @RJPoelstra
/tests/components/motionmount/ @RJPoelstra /tests/components/motionmount/ @RJPoelstra
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco /homeassistant/components/mqtt/ @emontnemery @jbouwh
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco /tests/components/mqtt/ @emontnemery @jbouwh
/homeassistant/components/msteams/ @peroyvind /homeassistant/components/msteams/ @peroyvind
/homeassistant/components/mullvad/ @meichthys /homeassistant/components/mullvad/ @meichthys
/tests/components/mullvad/ @meichthys /tests/components/mullvad/ @meichthys
@@ -973,8 +927,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/octoprint/ @rfleming71 /homeassistant/components/octoprint/ @rfleming71
/tests/components/octoprint/ @rfleming71 /tests/components/octoprint/ @rfleming71
/homeassistant/components/ohmconnect/ @robbiet480 /homeassistant/components/ohmconnect/ @robbiet480
/homeassistant/components/ollama/ @synesthesiam
/tests/components/ollama/ @synesthesiam
/homeassistant/components/ombi/ @larssont /homeassistant/components/ombi/ @larssont
/homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu /homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu
/tests/components/omnilogic/ @oliver84 @djtimca @gentoosu /tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
@@ -1041,8 +993,8 @@ build.json @home-assistant/supervisor
/tests/components/persistent_notification/ @home-assistant/core /tests/components/persistent_notification/ @home-assistant/core
/homeassistant/components/philips_js/ @elupus /homeassistant/components/philips_js/ @elupus
/tests/components/philips_js/ @elupus /tests/components/philips_js/ @elupus
/homeassistant/components/pi_hole/ @shenxn /homeassistant/components/pi_hole/ @johnluetke @shenxn
/tests/components/pi_hole/ @shenxn /tests/components/pi_hole/ @johnluetke @shenxn
/homeassistant/components/picnic/ @corneyl /homeassistant/components/picnic/ @corneyl
/tests/components/picnic/ @corneyl /tests/components/picnic/ @corneyl
/homeassistant/components/pilight/ @trekky12 /homeassistant/components/pilight/ @trekky12
@@ -1096,8 +1048,8 @@ build.json @home-assistant/supervisor
/tests/components/pvpc_hourly_pricing/ @azogue /tests/components/pvpc_hourly_pricing/ @azogue
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39 /homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
/tests/components/qbittorrent/ @geoffreylagaisse @finder39 /tests/components/qbittorrent/ @geoffreylagaisse @finder39
/homeassistant/components/qingping/ @bdraco /homeassistant/components/qingping/ @bdraco @skgsergio
/tests/components/qingping/ @bdraco /tests/components/qingping/ @bdraco @skgsergio
/homeassistant/components/qld_bushfire/ @exxamalte /homeassistant/components/qld_bushfire/ @exxamalte
/tests/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte
/homeassistant/components/qnap/ @disforw /homeassistant/components/qnap/ @disforw
@@ -1196,13 +1148,11 @@ build.json @home-assistant/supervisor
/tests/components/ruuvitag_ble/ @akx /tests/components/ruuvitag_ble/ @akx
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc /homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
/tests/components/rympro/ @OnFreund @elad-bar @maorcc /tests/components/rympro/ @OnFreund @elad-bar @maorcc
/homeassistant/components/sabnzbd/ @shaiu @jpbede /homeassistant/components/sabnzbd/ @shaiu
/tests/components/sabnzbd/ @shaiu @jpbede /tests/components/sabnzbd/ @shaiu
/homeassistant/components/saj/ @fredericvl /homeassistant/components/saj/ @fredericvl
/homeassistant/components/samsungtv/ @chemelli74 @epenet /homeassistant/components/samsungtv/ @chemelli74 @epenet
/tests/components/samsungtv/ @chemelli74 @epenet /tests/components/samsungtv/ @chemelli74 @epenet
/homeassistant/components/sanix/ @tomaszsluszniak
/tests/components/sanix/ @tomaszsluszniak
/homeassistant/components/scene/ @home-assistant/core /homeassistant/components/scene/ @home-assistant/core
/tests/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core
/homeassistant/components/schedule/ @home-assistant/core /homeassistant/components/schedule/ @home-assistant/core
@@ -1281,21 +1231,20 @@ build.json @home-assistant/supervisor
/tests/components/smappee/ @bsmappee /tests/components/smappee/ @bsmappee
/homeassistant/components/smart_meter_texas/ @grahamwetzler /homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler /tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @andrewsayre
/tests/components/smartthings/ @andrewsayre
/homeassistant/components/smarttub/ @mdz /homeassistant/components/smarttub/ @mdz
/tests/components/smarttub/ @mdz /tests/components/smarttub/ @mdz
/homeassistant/components/smarty/ @z0mbieprocess /homeassistant/components/smarty/ @z0mbieprocess
/homeassistant/components/smhi/ @gjohansson-ST /homeassistant/components/smhi/ @gjohansson-ST
/tests/components/smhi/ @gjohansson-ST /tests/components/smhi/ @gjohansson-ST
/homeassistant/components/sms/ @ocalvo /homeassistant/components/sms/ @ocalvo
/tests/components/sms/ @ocalvo
/homeassistant/components/snapcast/ @luar123 /homeassistant/components/snapcast/ @luar123
/tests/components/snapcast/ @luar123 /tests/components/snapcast/ @luar123
/homeassistant/components/snmp/ @nmaggioni
/tests/components/snmp/ @nmaggioni
/homeassistant/components/snooz/ @AustinBrunkhorst /homeassistant/components/snooz/ @AustinBrunkhorst
/tests/components/snooz/ @AustinBrunkhorst /tests/components/snooz/ @AustinBrunkhorst
/homeassistant/components/solaredge/ @frenck @bdraco /homeassistant/components/solaredge/ @frenck
/tests/components/solaredge/ @frenck @bdraco /tests/components/solaredge/ @frenck
/homeassistant/components/solaredge_local/ @drobtravels @scheric /homeassistant/components/solaredge_local/ @drobtravels @scheric
/homeassistant/components/solarlog/ @Ernst79 /homeassistant/components/solarlog/ @Ernst79
/tests/components/solarlog/ @Ernst79 /tests/components/solarlog/ @Ernst79
@@ -1307,8 +1256,8 @@ build.json @home-assistant/supervisor
/tests/components/sonarr/ @ctalkington /tests/components/sonarr/ @ctalkington
/homeassistant/components/songpal/ @rytilahti @shenxn /homeassistant/components/songpal/ @rytilahti @shenxn
/tests/components/songpal/ @rytilahti @shenxn /tests/components/songpal/ @rytilahti @shenxn
/homeassistant/components/sonos/ @jjlawren @peterager /homeassistant/components/sonos/ @jjlawren
/tests/components/sonos/ @jjlawren @peterager /tests/components/sonos/ @jjlawren
/homeassistant/components/soundtouch/ @kroimon /homeassistant/components/soundtouch/ @kroimon
/tests/components/soundtouch/ @kroimon /tests/components/soundtouch/ @kroimon
/homeassistant/components/spaceapi/ @fabaff /homeassistant/components/spaceapi/ @fabaff
@@ -1367,8 +1316,8 @@ build.json @home-assistant/supervisor
/tests/components/switchbee/ @jafar-atili /tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland /homeassistant/components/switchbot_cloud/ @SeraphicRav
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland /tests/components/switchbot_cloud/ @SeraphicRav
/homeassistant/components/switcher_kis/ @thecode /homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode /tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/switchmate/ @danielhiversen @qiz-li
@@ -1421,8 +1370,7 @@ build.json @home-assistant/supervisor
/tests/components/thermobeacon/ @bdraco /tests/components/thermobeacon/ @bdraco
/homeassistant/components/thermopro/ @bdraco @h3ss /homeassistant/components/thermopro/ @bdraco @h3ss
/tests/components/thermopro/ @bdraco @h3ss /tests/components/thermopro/ @bdraco @h3ss
/homeassistant/components/thethingsnetwork/ @angelnu /homeassistant/components/thethingsnetwork/ @fabaff
/tests/components/thethingsnetwork/ @angelnu
/homeassistant/components/thread/ @home-assistant/core /homeassistant/components/thread/ @home-assistant/core
/tests/components/thread/ @home-assistant/core /tests/components/thread/ @home-assistant/core
/homeassistant/components/tibber/ @danielhiversen /homeassistant/components/tibber/ @danielhiversen
@@ -1486,8 +1434,8 @@ build.json @home-assistant/supervisor
/tests/components/unifi/ @Kane610 /tests/components/unifi/ @Kane610
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL /homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk /homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @bdraco /homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
/tests/components/unifiprotect/ @bdraco /tests/components/unifiprotect/ @AngellusMortis @bdraco
/homeassistant/components/upb/ @gwww /homeassistant/components/upb/ @gwww
/tests/components/upb/ @gwww /tests/components/upb/ @gwww
/homeassistant/components/upc_connect/ @pvizeli @fabaff /homeassistant/components/upc_connect/ @pvizeli @fabaff
@@ -1593,8 +1541,8 @@ build.json @home-assistant/supervisor
/tests/components/wiz/ @sbidy /tests/components/wiz/ @sbidy
/homeassistant/components/wled/ @frenck /homeassistant/components/wled/ @frenck
/tests/components/wled/ @frenck /tests/components/wled/ @frenck
/homeassistant/components/wolflink/ @adamkrol93 @mtielen /homeassistant/components/wolflink/ @adamkrol93
/tests/components/wolflink/ @adamkrol93 @mtielen /tests/components/wolflink/ @adamkrol93
/homeassistant/components/workday/ @fabaff @gjohansson-ST /homeassistant/components/workday/ @fabaff @gjohansson-ST
/tests/components/workday/ @fabaff @gjohansson-ST /tests/components/workday/ @fabaff @gjohansson-ST
/homeassistant/components/worldclock/ @fabaff /homeassistant/components/worldclock/ @fabaff

View File

@@ -5,7 +5,7 @@
We as members, contributors, and leaders pledge to make participation in our We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socioeconomic status, identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity nationality, personal appearance, race, religion, or sexual identity
and orientation. and orientation.

View File

@@ -6,43 +6,47 @@ FROM ${BUILD_FROM}
# Synchronize with homeassistant/core.py:async_stop # Synchronize with homeassistant/core.py:async_stop
ENV \ ENV \
S6_SERVICES_GRACETIME=240000 \ S6_SERVICES_GRACETIME=240000
UV_SYSTEM_PYTHON=true
ARG QEMU_CPU ARG QEMU_CPU
# Install uv
RUN pip3 install uv==0.1.43
WORKDIR /usr/src WORKDIR /usr/src
## Setup Home Assistant Core dependencies ## Setup Home Assistant Core dependencies
COPY requirements.txt homeassistant/ COPY requirements.txt homeassistant/
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
RUN \ RUN \
uv pip install \ pip3 install \
--no-build \ --only-binary=:all: \
-r homeassistant/requirements.txt -r homeassistant/requirements.txt
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
RUN \ RUN \
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \ if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \
uv pip install homeassistant/home_assistant_*.whl; \ pip3 install homeassistant/home_assistant_frontend-*.whl; \
fi \
&& if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \
pip3 install homeassistant/home_assistant_intents-*.whl; \
fi \ fi \
&& if [ "${BUILD_ARCH}" = "i386" ]; then \ && if [ "${BUILD_ARCH}" = "i386" ]; then \
linux32 uv pip install \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
--no-build \ 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; \ -r homeassistant/requirements_all.txt; \
else \ else \
uv pip install \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
--no-build \ 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; \ -r homeassistant/requirements_all.txt; \
fi fi
## Setup Home Assistant Core ## Setup Home Assistant Core
COPY . homeassistant/ COPY . homeassistant/
RUN \ RUN \
uv pip install \ pip3 install \
--only-binary=:all: \
-e ./homeassistant \ -e ./homeassistant \
&& python3 -m compileall \ && python3 -m compileall \
homeassistant/homeassistant homeassistant/homeassistant

View File

@@ -22,7 +22,6 @@ RUN \
libavcodec-dev \ libavcodec-dev \
libavdevice-dev \ libavdevice-dev \
libavutil-dev \ libavutil-dev \
libgammu-dev \
libswscale-dev \ libswscale-dev \
libswresample-dev \ libswresample-dev \
libavfilter-dev \ libavfilter-dev \
@@ -35,30 +34,21 @@ RUN \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install uv
RUN pip3 install uv
WORKDIR /usr/src WORKDIR /usr/src
# Setup hass-release # Setup hass-release
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
&& uv pip install --system -e hass-release/ && pip3 install -e hass-release/
USER vscode WORKDIR /workspaces
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
RUN uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /tmp
# Install Python dependencies from requirements # Install Python dependencies from requirements
COPY requirements.txt ./ COPY requirements.txt ./
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
RUN uv pip install -r requirements.txt RUN pip3 install -r requirements.txt
COPY requirements_test.txt requirements_test_pre_commit.txt ./ COPY requirements_test.txt requirements_test_pre_commit.txt ./
RUN uv pip install -r requirements_test.txt RUN pip3 install -r requirements_test.txt
RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
WORKDIR /workspaces
# Set the default shell to bash instead of sh # Set the default shell to bash instead of sh
ENV SHELL /bin/bash ENV SHELL /bin/bash

View File

@@ -7,8 +7,6 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__, demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__. `tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|screenshot-states| |screenshot-states|
Featured integrations Featured integrations
@@ -27,4 +25,4 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
.. |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/dev/.github/assets/screenshot-states.png
:target: https://demo.home-assistant.io :target: https://demo.home-assistant.io
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png .. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
:target: https://home-assistant.io/integrations/ :target: https://home-assistant.io/integrations/

View File

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

View File

@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from contextlib import suppress
import faulthandler import faulthandler
import os import os
import sys import sys
@@ -147,7 +146,9 @@ def get_arguments() -> argparse.Namespace:
help="Skips validation of operating system", help="Skips validation of operating system",
) )
return parser.parse_args() arguments = parser.parse_args()
return arguments
def check_threads() -> None: def check_threads() -> None:
@@ -209,10 +210,8 @@ def main() -> int:
exit_code = runner.run(runtime_conf) exit_code = runner.run(runtime_conf)
faulthandler.disable() faulthandler.disable()
# It's possible for the fault file to disappear, so suppress obvious errors if os.path.getsize(fault_file_name) == 0:
with suppress(FileNotFoundError): os.remove(fault_file_name)
if os.path.getsize(fault_file_name) == 0:
os.remove(fault_file_name)
check_threads() check_threads()

View File

@@ -33,9 +33,9 @@ EVENT_USER_ADDED = "user_added"
EVENT_USER_UPDATED = "user_updated" EVENT_USER_UPDATED = "user_updated"
EVENT_USER_REMOVED = "user_removed" EVENT_USER_REMOVED = "user_removed"
type _MfaModuleDict = dict[str, MultiFactorAuthModule] _MfaModuleDict = dict[str, MultiFactorAuthModule]
type _ProviderKey = tuple[str, str | None] _ProviderKey = tuple[str, str | None]
type _ProviderDict = dict[_ProviderKey, AuthProvider] _ProviderDict = dict[_ProviderKey, AuthProvider]
class InvalidAuthError(Exception): class InvalidAuthError(Exception):
@@ -85,7 +85,7 @@ async def auth_manager_from_config(
module_hash[module.id] = module module_hash[module.id] = module
manager = AuthManager(hass, store, provider_hash, module_hash) manager = AuthManager(hass, store, provider_hash, module_hash)
await manager.async_setup() manager.async_setup()
return manager return manager
@@ -181,7 +181,8 @@ class AuthManager:
self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback
) )
async def async_setup(self) -> None: @callback
def async_setup(self) -> None:
"""Set up the auth manager.""" """Set up the auth manager."""
hass = self.hass hass = self.hass
hass.async_add_shutdown_job( hass.async_add_shutdown_job(
@@ -516,13 +517,6 @@ class AuthManager:
for revoke_callback in callbacks: for revoke_callback in callbacks:
revoke_callback() revoke_callback()
@callback
def async_set_expiry(
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
) -> None:
"""Enable or disable expiry of a refresh token."""
self._store.async_set_expiry(refresh_token, enable_expiry=enable_expiry)
@callback @callback
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None: def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
"""Remove expired refresh tokens.""" """Remove expired refresh tokens."""

View File

@@ -62,7 +62,6 @@ class AuthStore:
self._store = Store[dict[str, list[dict[str, Any]]]]( self._store = Store[dict[str, list[dict[str, Any]]]](
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
) )
self._token_id_to_user_id: dict[str, str] = {}
async def async_get_groups(self) -> list[models.Group]: async def async_get_groups(self) -> list[models.Group]:
"""Retrieve all users.""" """Retrieve all users."""
@@ -136,10 +135,7 @@ class AuthStore:
async def async_remove_user(self, user: models.User) -> None: async def async_remove_user(self, user: models.User) -> None:
"""Remove a user.""" """Remove a user."""
user = self._users.pop(user.id) self._users.pop(user.id)
for refresh_token_id in user.refresh_tokens:
del self._token_id_to_user_id[refresh_token_id]
user.refresh_tokens.clear()
self._async_schedule_save() self._async_schedule_save()
async def async_update_user( async def async_update_user(
@@ -222,9 +218,7 @@ class AuthStore:
kwargs["client_icon"] = client_icon kwargs["client_icon"] = client_icon
refresh_token = models.RefreshToken(**kwargs) refresh_token = models.RefreshToken(**kwargs)
token_id = refresh_token.id user.refresh_tokens[refresh_token.id] = refresh_token
user.refresh_tokens[token_id] = refresh_token
self._token_id_to_user_id[token_id] = user.id
self._async_schedule_save() self._async_schedule_save()
return refresh_token return refresh_token
@@ -232,17 +226,19 @@ class AuthStore:
@callback @callback
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None: def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
"""Remove a refresh token.""" """Remove a refresh token."""
refresh_token_id = refresh_token.id for user in self._users.values():
if user_id := self._token_id_to_user_id.get(refresh_token_id): if user.refresh_tokens.pop(refresh_token.id, None):
del self._users[user_id].refresh_tokens[refresh_token_id] self._async_schedule_save()
del self._token_id_to_user_id[refresh_token_id] break
self._async_schedule_save()
@callback @callback
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None: def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
"""Get refresh token by id.""" """Get refresh token by id."""
if user_id := self._token_id_to_user_id.get(token_id): for user in self._users.values():
return self._users[user_id].refresh_tokens.get(token_id) refresh_token = user.refresh_tokens.get(token_id)
if refresh_token is not None:
return refresh_token
return None return None
@callback @callback
@@ -281,21 +277,6 @@ class AuthStore:
) )
self._async_schedule_save() self._async_schedule_save()
@callback
def async_set_expiry(
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
) -> None:
"""Enable or disable expiry of a refresh token."""
if enable_expiry:
if refresh_token.expire_at is None:
refresh_token.expire_at = (
refresh_token.last_used_at or dt_util.utcnow()
).timestamp() + REFRESH_TOKEN_EXPIRATION
self._async_schedule_save()
else:
refresh_token.expire_at = None
self._async_schedule_save()
async def async_load(self) -> None: # noqa: C901 async def async_load(self) -> None: # noqa: C901
"""Load the users.""" """Load the users."""
if self._loaded: if self._loaded:
@@ -309,6 +290,8 @@ class AuthStore:
perm_lookup = PermissionLookup(ent_reg, dev_reg) perm_lookup = PermissionLookup(ent_reg, dev_reg)
self._perm_lookup = perm_lookup self._perm_lookup = perm_lookup
now_ts = dt_util.utcnow().timestamp()
if data is None or not isinstance(data, dict): if data is None or not isinstance(data, dict):
self._set_defaults() self._set_defaults()
return return
@@ -462,6 +445,14 @@ class AuthStore:
else: else:
last_used_at = None 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( token = models.RefreshToken(
id=rt_dict["id"], id=rt_dict["id"],
user=users[rt_dict["user_id"]], user=users[rt_dict["user_id"]],
@@ -478,7 +469,7 @@ class AuthStore:
jwt_key=rt_dict["jwt_key"], jwt_key=rt_dict["jwt_key"],
last_used_at=last_used_at, last_used_at=last_used_at,
last_used_ip=rt_dict.get("last_used_ip"), last_used_ip=rt_dict.get("last_used_ip"),
expire_at=rt_dict.get("expire_at"), expire_at=expire_at,
version=rt_dict.get("version"), version=rt_dict.get("version"),
) )
if "credential_id" in rt_dict: if "credential_id" in rt_dict:
@@ -487,17 +478,8 @@ class AuthStore:
self._groups = groups self._groups = groups
self._users = users self._users = users
self._build_token_id_to_user_id()
self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
@callback self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
def _build_token_id_to_user_id(self) -> None:
"""Build a map of token id to user id."""
self._token_id_to_user_id = {
token_id: user_id
for user_id, user in self._users.items()
for token_id in user.refresh_tokens
}
@callback @callback
def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None: def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None:
@@ -592,7 +574,6 @@ class AuthStore:
read_only_group = _system_read_only_group() read_only_group = _system_read_only_group()
groups[read_only_group.id] = read_only_group groups[read_only_group.id] = read_only_group
self._groups = groups self._groups = groups
self._build_token_id_to_user_id()
def _system_admin_group() -> models.Group: def _system_admin_group() -> models.Group:

View File

@@ -78,7 +78,7 @@ class _PyJWTWithVerify(PyJWT):
key: str, key: str,
algorithms: list[str], algorithms: list[str],
issuer: str | None = None, issuer: str | None = None,
leeway: float | timedelta = 0, leeway: int | float | timedelta = 0,
options: dict[str, Any] | None = None, options: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Verify a JWT's signature and claims.""" """Verify a JWT's signature and claims."""

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import importlib
import logging import logging
import types import types
from typing import Any from typing import Any
@@ -14,9 +15,7 @@ from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.importlib import async_import_module
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.util.hass_dict import HassKey
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry() MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
@@ -30,7 +29,7 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
DATA_REQS: HassKey[set[str]] = HassKey("mfa_auth_module_reqs_processed") DATA_REQS = "mfa_auth_module_reqs_processed"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -150,7 +149,7 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul
module_path = f"homeassistant.auth.mfa_modules.{module_name}" module_path = f"homeassistant.auth.mfa_modules.{module_name}"
try: try:
module = await async_import_module(hass, module_path) module = importlib.import_module(module_path)
except ImportError as err: except ImportError as err:
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err) _LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
raise HomeAssistantError( raise HomeAssistantError(

View File

@@ -88,7 +88,7 @@ class NotifySetting:
target: str | None = attr.ib(default=None) target: str | None = attr.ib(default=None)
type _UsersDict = dict[str, NotifySetting] _UsersDict = dict[str, NotifySetting]
@MULTI_FACTOR_AUTH_MODULES.register("notify") @MULTI_FACTOR_AUTH_MODULES.register("notify")

View File

@@ -3,9 +3,8 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import cached_property
import secrets import secrets
from typing import Any, NamedTuple from typing import TYPE_CHECKING, Any, NamedTuple
import uuid import uuid
import attr import attr
@@ -19,6 +18,12 @@ from homeassistant.util import dt as dt_util
from . import permissions as perm_mdl from . import permissions as perm_mdl
from .const import GROUP_ID_ADMIN 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_NORMAL = "normal"
TOKEN_TYPE_SYSTEM = "system" TOKEN_TYPE_SYSTEM = "system"
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
@@ -86,7 +91,11 @@ class User:
def invalidate_cache(self) -> None: def invalidate_cache(self) -> None:
"""Invalidate permission and is_admin cache.""" """Invalidate permission and is_admin cache."""
for attr_to_invalidate in ("permissions", "is_admin"): for attr_to_invalidate in ("permissions", "is_admin"):
self.__dict__.pop(attr_to_invalidate, None) # try is must more efficient than suppress
try: # noqa: SIM105
delattr(self, attr_to_invalidate)
except AttributeError:
pass
@attr.s(slots=True) @attr.s(slots=True)

View File

@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from typing import Any
import voluptuous as vol import voluptuous as vol
@@ -63,7 +64,7 @@ class PolicyPermissions(AbstractPermissions):
"""Return a function that can test entity access.""" """Return a function that can test entity access."""
return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup) return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup)
def __eq__(self, other: object) -> bool: def __eq__(self, other: Any) -> bool:
"""Equals check.""" """Equals check."""
return isinstance(other, PolicyPermissions) and other._policy == self._policy return isinstance(other, PolicyPermissions) and other._policy == self._policy

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Final from typing import Final
from homeassistant.const import ( from homeassistant.const import (
EVENT_COMPONENT_LOADED, EVENT_COMPONENT_LOADED,
@@ -21,11 +21,10 @@ from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
from homeassistant.util.event_type import EventType
# These are events that do not contain any sensitive data # These are events that do not contain any sensitive data
# Except for state_changed, which is handled accordingly. # Except for state_changed, which is handled accordingly.
SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = { SUBSCRIBE_ALLOWLIST: Final[set[str]] = {
EVENT_AREA_REGISTRY_UPDATED, EVENT_AREA_REGISTRY_UPDATED,
EVENT_COMPONENT_LOADED, EVENT_COMPONENT_LOADED,
EVENT_CORE_CONFIG_UPDATE, EVENT_CORE_CONFIG_UPDATE,

View File

@@ -58,7 +58,10 @@ def _merge_policies(sources: list[CategoryType]) -> CategoryType:
continue continue
seen.add(key) seen.add(key)
key_sources = [src.get(key) for src in sources if isinstance(src, dict)] key_sources = []
for src in sources:
if isinstance(src, dict):
key_sources.append(src.get(key))
policy[key] = _merge_policies(key_sources) policy[key] = _merge_policies(key_sources)

View File

@@ -4,17 +4,17 @@ from collections.abc import Mapping
# MyPy doesn't support recursion yet. So writing it out as far as we need. # MyPy doesn't support recursion yet. So writing it out as far as we need.
type ValueType = ( ValueType = (
# Example: entities.all = { read: true, control: true } # Example: entities.all = { read: true, control: true }
Mapping[str, bool] | bool | None Mapping[str, bool] | bool | None
) )
# Example: entities.domains = { light: … } # Example: entities.domains = { light: … }
type SubCategoryDict = Mapping[str, ValueType] SubCategoryDict = Mapping[str, ValueType]
type SubCategoryType = SubCategoryDict | bool | None SubCategoryType = SubCategoryDict | bool | None
type CategoryType = ( CategoryType = (
# Example: entities.domains # Example: entities.domains
Mapping[str, SubCategoryType] Mapping[str, SubCategoryType]
# Example: entities.all # Example: entities.all
@@ -24,4 +24,4 @@ type CategoryType = (
) )
# Example: { entities: … } # Example: { entities: … }
type PolicyType = Mapping[str, CategoryType] PolicyType = Mapping[str, CategoryType]

View File

@@ -10,8 +10,8 @@ from .const import SUBCAT_ALL
from .models import PermissionLookup from .models import PermissionLookup
from .types import CategoryType, SubCategoryDict, ValueType from .types import CategoryType, SubCategoryDict, ValueType
type LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None] LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
type SubCatLookupType = dict[str, LookupFunc] SubCatLookupType = dict[str, LookupFunc]
def lookup_all( def lookup_all(

View File

@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
import importlib
import logging import logging
import types import types
from typing import Any from typing import Any
@@ -14,17 +15,15 @@ from homeassistant import data_entry_flow, requirements
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.importlib import async_import_module
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.util.hass_dict import HassKey
from ..auth_store import AuthStore from ..auth_store import AuthStore
from ..const import MFA_SESSION_EXPIRATION from ..const import MFA_SESSION_EXPIRATION
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed") DATA_REQS = "auth_prov_reqs_processed"
AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry() AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry()
@@ -158,9 +157,7 @@ async def load_auth_provider_module(
) -> types.ModuleType: ) -> types.ModuleType:
"""Load an auth provider.""" """Load an auth provider."""
try: try:
module = await async_import_module( module = importlib.import_module(f"homeassistant.auth.providers.{provider}")
hass, f"homeassistant.auth.providers.{provider}"
)
except ImportError as err: except ImportError as err:
_LOGGER.error("Unable to load auth provider %s: %s", provider, err) _LOGGER.error("Unable to load auth provider %s: %s", provider, err)
raise HomeAssistantError( raise HomeAssistantError(

View File

@@ -28,8 +28,8 @@ from .. import InvalidAuthError
from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
type IPAddress = IPv4Address | IPv6Address IPAddress = IPv4Address | IPv6Address
type IPNetwork = IPv4Network | IPv6Network IPNetwork = IPv4Network | IPv6Network
CONF_TRUSTED_NETWORKS = "trusted_networks" CONF_TRUSTED_NETWORKS = "trusted_networks"
CONF_TRUSTED_USERS = "trusted_users" CONF_TRUSTED_USERS = "trusted_users"

View File

@@ -9,21 +9,8 @@ import it.
from __future__ import annotations from __future__ import annotations
from enum import StrEnum as _StrEnum from enum import StrEnum
from functools import partial
from homeassistant.helpers.deprecation import ( __all__ = [
DeprecatedAlias, "StrEnum",
all_with_deprecated_constants, ]
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
# StrEnum deprecated as of 2024.5 use enum.StrEnum instead.
_DEPRECATED_StrEnum = DeprecatedAlias(_StrEnum, "enum.StrEnum", "2025.5")
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
__dir__ = partial(
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
)
__all__ = all_with_deprecated_constants(globals())

View File

@@ -1,30 +1,81 @@
"""Functools backports from standard lib. """Functools backports from standard lib."""
This file contained the backport of the cached_property implementation of Python 3.12. # This file contains parts of Python's module wrapper
# for the _functools C module
Since we have dropped support for Python 3.11, we can remove this backport. # to allow utilities written in Python to be added
This file is kept for now to avoid breaking custom components that might # to the functools module.
import it. # Written by Nick Coghlan <ncoghlan at gmail.com>,
""" # Raymond Hettinger <python at rcn.com>,
# and Łukasz Langa <lukasz at langa.pl>.
# Copyright © 2001-2023 Python Software Foundation; All Rights Reserved
from __future__ import annotations from __future__ import annotations
from functools import cached_property as _cached_property, partial from collections.abc import Callable
from types import GenericAlias
from typing import Any, Generic, Self, TypeVar, overload
from homeassistant.helpers.deprecation import ( _T = TypeVar("_T")
DeprecatedAlias,
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
# cached_property deprecated as of 2024.5 use functools.cached_property instead.
_DEPRECATED_cached_property = DeprecatedAlias(
_cached_property, "functools.cached_property", "2025.5"
)
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) class cached_property(Generic[_T]):
__dir__ = partial( """Backport of Python 3.12's cached_property.
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
) Includes https://github.com/python/cpython/pull/101890/files
__all__ = all_with_deprecated_constants(globals()) """
def __init__(self, func: Callable[[Any], _T]) -> None:
"""Initialize."""
self.func: Callable[[Any], _T] = func
self.attrname: str | None = None
self.__doc__ = func.__doc__
def __set_name__(self, owner: type[Any], name: str) -> None:
"""Set name."""
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)
@overload
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self:
...
@overload
def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T:
...
def __get__(
self, instance: Any | None, owner: type[Any] | None = None
) -> _T | Self:
"""Get."""
if instance is None:
return self
if self.attrname is None:
raise TypeError(
"Cannot use cached_property instance without calling __set_name__ on it."
)
try:
cache = instance.__dict__
# not all objects have __dict__ (e.g. class defines slots)
except AttributeError:
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."
)
raise TypeError(msg) from None
return val
__class_getitem__ = classmethod(GenericAlias) # type: ignore[var-annotated]

View File

@@ -1,80 +1,21 @@
"""Block blocking calls being done in asyncio.""" """Block blocking calls being done in asyncio."""
import builtins
from contextlib import suppress
from http.client import HTTPConnection from http.client import HTTPConnection
import importlib
import sys
import threading
import time import time
from typing import Any
from .helpers.frame import get_current_frame from .util.async_ import protect_loop
from .util.loop import protect_loop
_IN_TESTS = "unittest" in sys.modules
ALLOWED_FILE_PREFIXES = ("/proc",)
def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
# If the module is already imported, we can ignore it.
return bool((args := mapped_args.get("args")) and args[0] in sys.modules)
def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
# If the file is in /proc we can ignore it.
args = mapped_args["args"]
path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
return path.startswith(ALLOWED_FILE_PREFIXES)
def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
#
# Avoid extracting the stack unless we need to since it
# will have to access the linecache which can do blocking
# I/O and we are trying to avoid blocking calls.
#
# frame[0] is us
# frame[1] is raise_for_blocking_call
# frame[2] is protected_loop_func
# frame[3] is the offender
with suppress(ValueError):
return get_current_frame(4).f_code.co_filename.endswith("pydevd.py")
return False
def enable() -> None: def enable() -> None:
"""Enable the detection of blocking calls in the event loop.""" """Enable the detection of blocking calls in the event loop."""
loop_thread_id = threading.get_ident()
# Prevent urllib3 and requests doing I/O in event loop # Prevent urllib3 and requests doing I/O in event loop
HTTPConnection.putrequest = protect_loop( # type: ignore[method-assign] HTTPConnection.putrequest = protect_loop( # type: ignore[method-assign]
HTTPConnection.putrequest, loop_thread_id=loop_thread_id HTTPConnection.putrequest
) )
# Prevent sleeping in event loop. Non-strict since 2022.02 # Prevent sleeping in event loop. Non-strict since 2022.02
time.sleep = protect_loop( time.sleep = protect_loop(time.sleep, strict=False)
time.sleep,
strict=False,
check_allowed=_check_sleep_call_allowed,
loop_thread_id=loop_thread_id,
)
if not _IN_TESTS: # Currently disabled. pytz doing I/O when getting timezone.
# Prevent files being opened inside the event loop # Prevent files being opened inside the event loop
builtins.open = protect_loop( # type: ignore[assignment] # builtins.open = protect_loop(builtins.open)
builtins.open,
strict_core=False,
strict=False,
check_allowed=_check_file_allowed,
loop_thread_id=loop_thread_id,
)
# unittest uses `importlib.import_module` to do mocking
# so we cannot protect it if we are running tests
importlib.import_module = protect_loop(
importlib.import_module,
strict_core=False,
strict=False,
check_allowed=_check_import_call_allowed,
loop_thread_id=loop_thread_id,
)

View File

@@ -3,14 +3,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections import defaultdict
import contextlib import contextlib
from functools import partial from datetime import timedelta
from itertools import chain
import logging import logging
import logging.handlers import logging.handlers
import mimetypes from operator import itemgetter
from operator import contains, itemgetter
import os import os
import platform import platform
import sys import sys
@@ -24,14 +21,7 @@ import cryptography.hazmat.backends.openssl.backend # noqa: F401
import voluptuous as vol import voluptuous as vol
import yarl import yarl
from . import ( from . import config as conf_util, config_entries, core, loader, requirements
block_async_io,
config as conf_util,
config_entries,
core,
loader,
requirements,
)
# Pre-import frontend deps which have no requirements here to avoid # Pre-import frontend deps which have no requirements here to avoid
# loading them at run time and blocking the event loop. We do this ahead # loading them at run time and blocking the event loop. We do this ahead
@@ -42,11 +32,9 @@ from .components import (
api as api_pre_import, # noqa: F401 api as api_pre_import, # noqa: F401
auth as auth_pre_import, # noqa: F401 auth as auth_pre_import, # noqa: F401
config as config_pre_import, # noqa: F401 config as config_pre_import, # noqa: F401
default_config as default_config_pre_import, # noqa: F401
device_automation as device_automation_pre_import, # noqa: F401 device_automation as device_automation_pre_import, # noqa: F401
diagnostics as diagnostics_pre_import, # noqa: F401 diagnostics as diagnostics_pre_import, # noqa: F401
file_upload as file_upload_pre_import, # noqa: F401 file_upload as file_upload_pre_import, # noqa: F401
group as group_pre_import, # noqa: F401
history as history_pre_import, # noqa: F401 history as history_pre_import, # noqa: F401
http, # not named pre_import since it has requirements http, # not named pre_import since it has requirements
image_upload as image_upload_import, # noqa: F401 - not named pre_import since it has requirements image_upload as image_upload_import, # noqa: F401 - not named pre_import since it has requirements
@@ -63,7 +51,6 @@ from .components import (
) )
from .components.sensor import recorder as sensor_recorder # noqa: F401 from .components.sensor import recorder as sensor_recorder # noqa: F401
from .const import ( from .const import (
BASE_PLATFORMS,
FORMAT_DATETIME, FORMAT_DATETIME,
KEY_DATA_LOGGING as DATA_LOGGING, KEY_DATA_LOGGING as DATA_LOGGING,
REQUIRED_NEXT_PYTHON_HA_RELEASE, REQUIRED_NEXT_PYTHON_HA_RELEASE,
@@ -73,7 +60,6 @@ from .const import (
from .exceptions import HomeAssistantError from .exceptions import HomeAssistantError
from .helpers import ( from .helpers import (
area_registry, area_registry,
category_registry,
config_validation as cv, config_validation as cv,
device_registry, device_registry,
entity, entity,
@@ -86,43 +72,29 @@ from .helpers import (
template, template,
translation, translation,
) )
from .helpers.dispatcher import async_dispatcher_send_internal from .helpers.dispatcher import async_dispatcher_send
from .helpers.storage import get_internal_store_manager
from .helpers.system_info import async_get_system_info
from .helpers.typing import ConfigType from .helpers.typing import ConfigType
from .setup import ( from .setup import (
# _setup_started is marked as protected to make it clear BASE_PLATFORMS,
# that it is not part of the public API and should not be used DATA_SETUP_STARTED,
# by integrations. It is only used for internal tracking of DATA_SETUP_TIME,
# which integrations are being set up.
_setup_started,
async_get_setup_timings,
async_notify_setup_error, async_notify_setup_error,
async_set_domains_to_be_loaded, async_set_domains_to_be_loaded,
async_setup_component, async_setup_component,
) )
from .util.async_ import create_eager_task from .util.async_ import create_eager_task
from .util.hass_dict import HassKey
from .util.logging import async_activate_log_queue_handler from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_virtual_env from .util.package import async_get_user_site, is_virtual_env
with contextlib.suppress(ImportError):
# Ensure anyio backend is imported to avoid it being imported in the event loop
from anyio._backends import _asyncio # noqa: F401
if TYPE_CHECKING: if TYPE_CHECKING:
from .runner import RuntimeConfig from .runner import RuntimeConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS)
ERROR_LOG_FILENAME = "home-assistant.log" ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information. # hass.data key for logging information.
DATA_REGISTRIES_LOADED: HassKey[None] = HassKey("bootstrap_registries_loaded") DATA_REGISTRIES_LOADED = "bootstrap_registries_loaded"
LOG_SLOW_STARTUP_INTERVAL = 60 LOG_SLOW_STARTUP_INTERVAL = 60
SLOW_STARTUP_CHECK_INTERVAL = 1 SLOW_STARTUP_CHECK_INTERVAL = 1
@@ -213,36 +185,16 @@ CRITICAL_INTEGRATIONS = {
"frontend", "frontend",
} }
SETUP_ORDER = ( SETUP_ORDER = {
# Load logging as soon as possible # Load logging as soon as possible
("logging", LOGGING_INTEGRATIONS), "logging": LOGGING_INTEGRATIONS,
# Setup frontend and recorder # Setup frontend
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}), "frontend": FRONTEND_INTEGRATIONS,
# Setup recorder
"recorder": RECORDER_INTEGRATIONS,
# Start up debuggers. Start these first in case they want to wait. # Start up debuggers. Start these first in case they want to wait.
("debugger", DEBUGGER_INTEGRATIONS), "debugger": DEBUGGER_INTEGRATIONS,
) }
#
# Storage keys we are likely to load during startup
# in order of when we expect to load them.
#
# If they do not exist they will not be loaded
#
PRELOAD_STORAGE = [
"core.logger",
"core.network",
"http.auth",
"image",
"lovelace_dashboards",
"lovelace_resources",
"core.uuid",
"lovelace.map",
"bluetooth.passive_update_processor",
"bluetooth.remote_scanners",
"assist_pipeline.pipelines",
"core.analytics",
"auth_module.totp",
]
async def async_setup_hass( async def async_setup_hass(
@@ -259,9 +211,6 @@ async def async_setup_hass(
runtime_config.log_no_color, runtime_config.log_no_color,
) )
if runtime_config.debug or hass.loop.get_debug():
hass.config.debug = True
hass.config.safe_mode = runtime_config.safe_mode hass.config.safe_mode = runtime_config.safe_mode
hass.config.skip_pip = runtime_config.skip_pip hass.config.skip_pip = runtime_config.skip_pip
hass.config.skip_pip_packages = runtime_config.skip_pip_packages hass.config.skip_pip_packages = runtime_config.skip_pip_packages
@@ -277,8 +226,6 @@ async def async_setup_hass(
_LOGGER.info("Config directory: %s", runtime_config.config_dir) _LOGGER.info("Config directory: %s", runtime_config.config_dir)
loader.async_setup(hass) loader.async_setup(hass)
block_async_io.enable()
config_dict = None config_dict = None
basic_setup_success = False basic_setup_success = False
@@ -325,7 +272,6 @@ async def async_setup_hass(
hass = core.HomeAssistant(old_config.config_dir) hass = core.HomeAssistant(old_config.config_dir)
if old_logging: if old_logging:
hass.data[DATA_LOGGING] = old_logging hass.data[DATA_LOGGING] = old_logging
hass.config.debug = old_config.debug
hass.config.skip_pip = old_config.skip_pip hass.config.skip_pip = old_config.skip_pip
hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.skip_pip_packages = old_config.skip_pip_packages
hass.config.internal_url = old_config.internal_url hass.config.internal_url = old_config.internal_url
@@ -372,41 +318,37 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
) )
def _init_blocking_io_modules_in_executor() -> None:
"""Initialize modules that do blocking I/O in executor."""
# Cache the result of platform.uname().processor in the executor.
# Multiple modules call this function at startup which
# executes a blocking subprocess call. This is a problem for the
# asyncio event loop. By priming the cache of uname we can
# avoid the blocking call in the event loop.
_ = platform.uname().processor
# Initialize the mimetypes module to avoid blocking calls
# to the filesystem to load the mime.types file.
mimetypes.init()
async def async_load_base_functionality(hass: core.HomeAssistant) -> None: async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
"""Load the registries and modules that will do blocking I/O.""" """Load the registries and cache the result of platform.uname().processor."""
if DATA_REGISTRIES_LOADED in hass.data: if DATA_REGISTRIES_LOADED in hass.data:
return return
hass.data[DATA_REGISTRIES_LOADED] = None hass.data[DATA_REGISTRIES_LOADED] = None
def _cache_uname_processor() -> None:
"""Cache the result of platform.uname().processor in the executor.
Multiple modules call this function at startup which
executes a blocking subprocess call. This is a problem for the
asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop.
"""
platform.uname().processor # pylint: disable=expression-not-assigned
# Load the registries and cache the result of platform.uname().processor
translation.async_setup(hass) translation.async_setup(hass)
entity.async_setup(hass) entity.async_setup(hass)
template.async_setup(hass) template.async_setup(hass)
await asyncio.gather( await asyncio.gather(
create_eager_task(get_internal_store_manager(hass).async_initialize()),
create_eager_task(area_registry.async_load(hass)), create_eager_task(area_registry.async_load(hass)),
create_eager_task(category_registry.async_load(hass)),
create_eager_task(device_registry.async_load(hass)), create_eager_task(device_registry.async_load(hass)),
create_eager_task(entity_registry.async_load(hass)), create_eager_task(entity_registry.async_load(hass)),
create_eager_task(floor_registry.async_load(hass)), create_eager_task(floor_registry.async_load(hass)),
create_eager_task(issue_registry.async_load(hass)), create_eager_task(issue_registry.async_load(hass)),
create_eager_task(label_registry.async_load(hass)), create_eager_task(label_registry.async_load(hass)),
hass.async_add_executor_job(_init_blocking_io_modules_in_executor), hass.async_add_executor_job(_cache_uname_processor),
create_eager_task(template.async_load_custom_templates(hass)), create_eager_task(template.async_load_custom_templates(hass)),
create_eager_task(restore_state.async_load(hass)), create_eager_task(restore_state.async_load(hass)),
create_eager_task(hass.config_entries.async_initialize()), create_eager_task(hass.config_entries.async_initialize()),
create_eager_task(async_get_system_info(hass)),
) )
@@ -421,9 +363,6 @@ async def async_from_config_dict(
start = monotonic() start = monotonic()
hass.config_entries = config_entries.ConfigEntries(hass, config) hass.config_entries = config_entries.ConfigEntries(hass, config)
# Prime custom component cache early so we know if registry entries are tied
# to a custom integration
await loader.async_get_custom_components(hass)
await async_load_base_functionality(hass) await async_load_base_functionality(hass)
# Set up core. # Set up core.
@@ -432,11 +371,7 @@ async def async_from_config_dict(
if not all( if not all(
await asyncio.gather( await asyncio.gather(
*( *(
create_eager_task( create_eager_task(async_setup_component(hass, domain, config))
async_setup_component(hass, domain, config),
name=f"bootstrap setup {domain}",
loop=hass.loop,
)
for domain in CORE_INTEGRATIONS for domain in CORE_INTEGRATIONS
) )
) )
@@ -592,7 +527,7 @@ def async_enable_logging(
err_log_path, when="midnight", backupCount=log_rotate_days err_log_path, when="midnight", backupCount=log_rotate_days
) )
else: else:
err_handler = _RotatingFileHandlerWithoutShouldRollOver( err_handler = logging.handlers.RotatingFileHandler(
err_log_path, backupCount=1 err_log_path, backupCount=1
) )
@@ -616,19 +551,6 @@ def async_enable_logging(
async_activate_log_queue_handler(hass) async_activate_log_queue_handler(hass)
class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler):
"""RotatingFileHandler that does not check if it should roll over on every log."""
def shouldRollover(self, record: logging.LogRecord) -> bool:
"""Never roll over.
The shouldRollover check is expensive because it has to stat
the log file for every log record. Since we do not set maxBytes
the result of this check is always False.
"""
return False
async def async_mount_local_lib_path(config_dir: str) -> str: async def async_mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path. """Add local library to Python Path.
@@ -666,9 +588,7 @@ class _WatchPendingSetups:
"""Periodic log and dispatch of setups that are pending.""" """Periodic log and dispatch of setups that are pending."""
def __init__( def __init__(
self, self, hass: core.HomeAssistant, setup_started: dict[str, float]
hass: core.HomeAssistant,
setup_started: dict[tuple[str, str | None], float],
) -> None: ) -> None:
"""Initialize the WatchPendingSetups class.""" """Initialize the WatchPendingSetups class."""
self._hass = hass self._hass = hass
@@ -683,14 +603,13 @@ class _WatchPendingSetups:
now = monotonic() now = monotonic()
self._duration_count += SLOW_STARTUP_CHECK_INTERVAL self._duration_count += SLOW_STARTUP_CHECK_INTERVAL
remaining_with_setup_started: defaultdict[str, float] = defaultdict(float) remaining_with_setup_started = {
for integration_group, start_time in self._setup_started.items(): domain: (now - start_time)
domain, _ = integration_group for domain, start_time in self._setup_started.items()
remaining_with_setup_started[domain] += now - start_time }
if remaining_with_setup_started: if remaining_with_setup_started:
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
elif waiting_tasks := self._hass._active_tasks: # noqa: SLF001 elif waiting_tasks := self._hass._active_tasks: # pylint: disable=protected-access
_LOGGER.debug("Waiting on tasks: %s", waiting_tasks) _LOGGER.debug("Waiting on tasks: %s", waiting_tasks)
self._async_dispatch(remaining_with_setup_started) self._async_dispatch(remaining_with_setup_started)
if ( if (
@@ -701,7 +620,7 @@ class _WatchPendingSetups:
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up # once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
_LOGGER.warning( _LOGGER.warning(
"Waiting on integrations to complete setup: %s", "Waiting on integrations to complete setup: %s",
self._setup_started, ", ".join(self._setup_started),
) )
_LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones) _LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones)
@@ -710,7 +629,7 @@ class _WatchPendingSetups:
def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None: def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None:
"""Dispatch the signal.""" """Dispatch the signal."""
if remaining_with_setup_started or not self._previous_was_empty: if remaining_with_setup_started or not self._previous_was_empty:
async_dispatcher_send_internal( async_dispatcher_send(
self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started
) )
self._previous_was_empty = not remaining_with_setup_started self._previous_was_empty = not remaining_with_setup_started
@@ -741,18 +660,13 @@ async def async_setup_multi_components(
"""Set up multiple domains. Log on failure.""" """Set up multiple domains. Log on failure."""
# Avoid creating tasks for domains that were setup in a previous stage # Avoid creating tasks for domains that were setup in a previous stage
domains_not_yet_setup = domains - hass.config.components domains_not_yet_setup = domains - hass.config.components
# Create setup tasks for base platforms first since everything will have
# to wait to be imported, and the sooner we can get the base platforms
# loaded the sooner we can start loading the rest of the integrations.
futures = { futures = {
domain: hass.async_create_task_internal( domain: hass.async_create_task(
async_setup_component(hass, domain, config), async_setup_component(hass, domain, config),
f"setup component {domain}", f"setup component {domain}",
eager_start=True, eager_start=True,
) )
for domain in sorted( for domain in domains_not_yet_setup
domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True
)
} }
results = await asyncio.gather(*futures.values(), return_exceptions=True) results = await asyncio.gather(*futures.values(), return_exceptions=True)
for idx, domain in enumerate(futures): for idx, domain in enumerate(futures):
@@ -769,53 +683,29 @@ async def _async_resolve_domains_to_setup(
hass: core.HomeAssistant, config: dict[str, Any] hass: core.HomeAssistant, config: dict[str, Any]
) -> tuple[set[str], dict[str, loader.Integration]]: ) -> tuple[set[str], dict[str, loader.Integration]]:
"""Resolve all dependencies and return list of domains to set up.""" """Resolve all dependencies and return list of domains to set up."""
base_platforms_loaded = False
domains_to_setup = _get_domains(hass, config) domains_to_setup = _get_domains(hass, config)
needed_requirements: set[str] = set() needed_requirements: set[str] = set()
platform_integrations = conf_util.extract_platform_integrations( platform_integrations = conf_util.extract_platform_integrations(
config, BASE_PLATFORMS config, BASE_PLATFORMS
) )
# Ensure base platforms that have platform integrations are added to
# to `domains_to_setup so they can be setup first instead of
# discovering them when later when a config entry setup task
# notices its needed and there is already a long line to use
# the import executor.
#
# For example if we have
# sensor:
# - platform: template
#
# `template` has to be loaded to validate the config for sensor
# so we want to start loading `sensor` as soon as we know
# it will be needed. The more platforms under `sensor:`, the longer
# it will take to finish setup for `sensor` because each of these
# platforms has to be imported before we can validate the config.
#
# Thankfully we are migrating away from the platform pattern
# so this will be less of a problem in the future.
domains_to_setup.update(platform_integrations)
# Load manifests for base platforms and platform based integrations
# that are defined under base platforms right away since we do not require
# the manifest to list them as dependencies and we want to avoid the lock
# contention when multiple integrations try to load them at once
additional_manifests_to_load = {
*BASE_PLATFORMS,
*chain.from_iterable(platform_integrations.values()),
}
translations_to_load = additional_manifests_to_load.copy()
# Resolve all dependencies so we know all integrations # Resolve all dependencies so we know all integrations
# that will have to be loaded and start right-away # that will have to be loaded and start rightaway
integration_cache: dict[str, loader.Integration] = {} integration_cache: dict[str, loader.Integration] = {}
to_resolve: set[str] = domains_to_setup to_resolve: set[str] = domains_to_setup
while to_resolve or additional_manifests_to_load: while to_resolve:
old_to_resolve: set[str] = to_resolve old_to_resolve: set[str] = to_resolve
to_resolve = set() to_resolve = set()
if additional_manifests_to_load: if not base_platforms_loaded:
to_get = {*old_to_resolve, *additional_manifests_to_load} # Load base platforms right away since
additional_manifests_to_load.clear() # we do not require the manifest to list
# them as dependencies and we want
# to avoid the lock contention when multiple
# integrations try to resolve them at once
base_platforms_loaded = True
to_get = {*old_to_resolve, *BASE_PLATFORMS, *platform_integrations}
else: else:
to_get = old_to_resolve to_get = old_to_resolve
@@ -828,17 +718,6 @@ async def _async_resolve_domains_to_setup(
continue continue
integration_cache[domain] = itg integration_cache[domain] = itg
needed_requirements.update(itg.requirements) needed_requirements.update(itg.requirements)
# Make sure manifests for dependencies are loaded in the next
# loop to try to group as many as manifest loads in a single
# call to avoid the creating one-off executor jobs later in
# the setup process
additional_manifests_to_load.update(
dep
for dep in chain(itg.dependencies, itg.after_dependencies)
if dep not in integration_cache
)
if domain not in old_to_resolve: if domain not in old_to_resolve:
continue continue
@@ -887,12 +766,6 @@ async def _async_resolve_domains_to_setup(
"check installed requirements", "check installed requirements",
eager_start=True, eager_start=True,
) )
#
# Only add the domains_to_setup after we finish resolving
# as new domains are likely to added in the process
#
translations_to_load.update(domains_to_setup)
# Start loading translations for all integrations we are going to set up # Start loading translations for all integrations we are going to set up
# in the background so they are ready when we need them. This avoids a # in the background so they are ready when we need them. This avoids a
# lot of waiting for the translation load lock and a thundering herd of # lot of waiting for the translation load lock and a thundering herd of
@@ -904,19 +777,10 @@ async def _async_resolve_domains_to_setup(
# wait for the translation load lock, loading will be done by the # wait for the translation load lock, loading will be done by the
# time it gets to it. # time it gets to it.
hass.async_create_background_task( hass.async_create_background_task(
translation.async_load_integrations(hass, translations_to_load), translation.async_load_integrations(
"load translations", hass, {*BASE_PLATFORMS, *platform_integrations, *domains_to_setup}
eager_start=True,
)
# Preload storage for all integrations we are going to set up
# so we do not have to wait for it to be loaded when we need it
# in the setup process.
hass.async_create_background_task(
get_internal_store_manager(hass).async_preload(
[*PRELOAD_STORAGE, *domains_to_setup]
), ),
"preload storage", "load translations",
eager_start=True, eager_start=True,
) )
@@ -927,7 +791,11 @@ async def _async_set_up_integrations(
hass: core.HomeAssistant, config: dict[str, Any] hass: core.HomeAssistant, config: dict[str, Any]
) -> None: ) -> None:
"""Set up all the integrations.""" """Set up all the integrations."""
watcher = _WatchPendingSetups(hass, _setup_started(hass)) setup_started: dict[str, float] = {}
hass.data[DATA_SETUP_STARTED] = setup_started
setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {})
watcher = _WatchPendingSetups(hass, setup_started)
watcher.async_start() watcher.async_start()
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup( domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
@@ -938,9 +806,10 @@ async def _async_set_up_integrations(
if "recorder" in domains_to_setup: if "recorder" in domains_to_setup:
recorder.async_initialize_recorder(hass) recorder.async_initialize_recorder(hass)
pre_stage_domains = [ pre_stage_domains: dict[str, set[str]] = {
(name, domains_to_setup & domain_group) for name, domain_group in SETUP_ORDER name: domains_to_setup & domain_group
] for name, domain_group in SETUP_ORDER.items()
}
# calculate what components to setup in what stage # calculate what components to setup in what stage
stage_1_domains: set[str] = set() stage_1_domains: set[str] = set()
@@ -966,18 +835,10 @@ async def _async_set_up_integrations(
stage_2_domains = domains_to_setup - stage_1_domains stage_2_domains = domains_to_setup - stage_1_domains
for name, domain_group in pre_stage_domains: for name, domain_group in pre_stage_domains.items():
if domain_group: if domain_group:
stage_2_domains -= domain_group stage_2_domains -= domain_group
_LOGGER.info("Setting up %s: %s", name, domain_group) _LOGGER.info("Setting up %s: %s", name, domain_group)
to_be_loaded = domain_group.copy()
to_be_loaded.update(
dep
for domain in domain_group
if (integration := integration_cache.get(domain)) is not None
for dep in integration.all_dependencies
)
async_set_domains_to_be_loaded(hass, to_be_loaded)
await async_setup_multi_components(hass, domain_group, config) await async_setup_multi_components(hass, domain_group, config)
# Enables after dependencies when setting up stage 1 domains # Enables after dependencies when setting up stage 1 domains
@@ -994,7 +855,7 @@ async def _async_set_up_integrations(
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(
"Setup timed out for stage 1 waiting on %s - moving forward", "Setup timed out for stage 1 waiting on %s - moving forward",
hass._active_tasks, # noqa: SLF001 hass._active_tasks, # pylint: disable=protected-access
) )
# Add after dependencies when setting up stage 2 domains # Add after dependencies when setting up stage 2 domains
@@ -1010,7 +871,7 @@ async def _async_set_up_integrations(
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(
"Setup timed out for stage 2 waiting on %s - moving forward", "Setup timed out for stage 2 waiting on %s - moving forward",
hass._active_tasks, # noqa: SLF001 hass._active_tasks, # pylint: disable=protected-access
) )
# Wrap up startup # Wrap up startup
@@ -1021,14 +882,12 @@ async def _async_set_up_integrations(
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(
"Setup timed out for bootstrap waiting on %s - moving forward", "Setup timed out for bootstrap waiting on %s - moving forward",
hass._active_tasks, # noqa: SLF001 hass._active_tasks, # pylint: disable=protected-access
) )
watcher.async_stop() watcher.async_stop()
if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug(
setup_time = async_get_setup_timings(hass) "Integration setup times: %s",
_LOGGER.debug( dict(sorted(setup_time.items(), key=itemgetter(1))),
"Integration setup times: %s", )
dict(sorted(setup_time.items(), key=itemgetter(1), reverse=True)),
)

View File

@@ -0,0 +1,5 @@
{
"domain": "epson",
"name": "Epson",
"integrations": ["epson", "epsonworkforce"]
}

View File

@@ -1,5 +1,5 @@
{ {
"domain": "eq3", "domain": "eq3",
"name": "eQ-3", "name": "eQ-3",
"integrations": ["maxcube", "eq3btsmart"] "integrations": ["maxcube"]
} }

View File

@@ -1,5 +0,0 @@
{
"domain": "motionblinds",
"name": "Motionblinds",
"integrations": ["motion_blinds", "motionblinds_ble"]
}

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import partial from functools import partial
from jaraco.abode.automation import Automation as AbodeAuto
from jaraco.abode.client import Client as Abode from jaraco.abode.client import Client as Abode
from jaraco.abode.devices.base import Device as AbodeDev
from jaraco.abode.exceptions import ( from jaraco.abode.exceptions import (
AuthenticationException as AbodeAuthenticationException, AuthenticationException as AbodeAuthenticationException,
Exception as AbodeException, Exception as AbodeException,
@@ -27,11 +29,11 @@ from homeassistant.const import (
) )
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import CONF_POLLING, DOMAIN, LOGGER from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
SERVICE_SETTINGS = "change_setting" SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_CAPTURE_IMAGE = "capture_image"
@@ -81,12 +83,6 @@ class AbodeSystem:
logout_listener: CALLBACK_TYPE | None = None logout_listener: CALLBACK_TYPE | None = None
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Abode component."""
setup_hass_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Abode integration from a config entry.""" """Set up Abode integration from a config entry."""
username = entry.data[CONF_USERNAME] username = entry.data[CONF_USERNAME]
@@ -115,6 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await setup_hass_events(hass) await setup_hass_events(hass)
await hass.async_add_executor_job(setup_hass_services, hass)
await hass.async_add_executor_job(setup_abode_events, hass) await hass.async_add_executor_job(setup_abode_events, hass)
return True return True
@@ -122,6 +119,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
hass.services.async_remove(DOMAIN, SERVICE_SETTINGS)
hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE)
hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_AUTOMATION)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop)
@@ -174,15 +175,15 @@ def setup_hass_services(hass: HomeAssistant) -> None:
signal = f"abode_trigger_automation_{entity_id}" signal = f"abode_trigger_automation_{entity_id}"
dispatcher_send(hass, signal) dispatcher_send(hass, signal)
hass.services.async_register( hass.services.register(
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
) )
hass.services.async_register( hass.services.register(
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
) )
hass.services.async_register( hass.services.register(
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
) )
@@ -246,3 +247,108 @@ def setup_abode_events(hass: HomeAssistant) -> None:
hass.data[DOMAIN].abode.events.add_event_callback( hass.data[DOMAIN].abode.events.add_event_callback(
event, partial(event_callback, event) event, partial(event_callback, event)
) )
class AbodeEntity(entity.Entity):
"""Representation of an Abode entity."""
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
def __init__(self, data: AbodeSystem) -> None:
"""Initialize Abode entity."""
self._data = data
self._attr_should_poll = data.polling
async def async_added_to_hass(self) -> None:
"""Subscribe to Abode connection status updates."""
await self.hass.async_add_executor_job(
self._data.abode.events.add_connection_status_callback,
self.unique_id,
self._update_connection_status,
)
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from Abode connection status updates."""
await self.hass.async_add_executor_job(
self._data.abode.events.remove_connection_status_callback, self.unique_id
)
def _update_connection_status(self) -> None:
"""Update the entity available property."""
self._attr_available = self._data.abode.events.connected
self.schedule_update_ha_state()
class AbodeDevice(AbodeEntity):
"""Representation of an Abode device."""
def __init__(self, data: AbodeSystem, device: AbodeDev) -> None:
"""Initialize Abode device."""
super().__init__(data)
self._device = device
self._attr_unique_id = device.uuid
async def async_added_to_hass(self) -> None:
"""Subscribe to device events."""
await super().async_added_to_hass()
await self.hass.async_add_executor_job(
self._data.abode.events.add_device_callback,
self._device.id,
self._update_callback,
)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from device events."""
await super().async_will_remove_from_hass()
await self.hass.async_add_executor_job(
self._data.abode.events.remove_all_device_callbacks, self._device.id
)
def update(self) -> None:
"""Update device state."""
self._device.refresh()
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
return {
"device_id": self._device.id,
"battery_low": self._device.battery_low,
"no_response": self._device.no_response,
"device_type": self._device.type,
}
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self._device.id)},
manufacturer="Abode",
model=self._device.type,
name=self._device.name,
)
def _update_callback(self, device: AbodeDev) -> None:
"""Update the device state."""
self.schedule_update_ha_state()
class AbodeAutomation(AbodeEntity):
"""Representation of an Abode automation."""
def __init__(self, data: AbodeSystem, automation: AbodeAuto) -> None:
"""Initialize for Abode automation."""
super().__init__(data)
self._automation = automation
self._attr_name = automation.name
self._attr_unique_id = automation.automation_id
self._attr_extra_state_attributes = {
"type": "CUE automation",
}
def update(self) -> None:
"""Update automation state."""
self._automation.refresh()

View File

@@ -17,9 +17,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(

View File

@@ -22,9 +22,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum
from . import AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(

View File

@@ -19,9 +19,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .entity import AbodeDevice
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)

View File

@@ -10,9 +10,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(

View File

@@ -1,115 +0,0 @@
"""Support for Abode Security System entities."""
from jaraco.abode.automation import Automation as AbodeAuto
from jaraco.abode.devices.base import Device as AbodeDev
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from . import AbodeSystem
from .const import ATTRIBUTION, DOMAIN
class AbodeEntity(Entity):
"""Representation of an Abode entity."""
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
def __init__(self, data: AbodeSystem) -> None:
"""Initialize Abode entity."""
self._data = data
self._attr_should_poll = data.polling
async def async_added_to_hass(self) -> None:
"""Subscribe to Abode connection status updates."""
await self.hass.async_add_executor_job(
self._data.abode.events.add_connection_status_callback,
self.unique_id,
self._update_connection_status,
)
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from Abode connection status updates."""
await self.hass.async_add_executor_job(
self._data.abode.events.remove_connection_status_callback, self.unique_id
)
def _update_connection_status(self) -> None:
"""Update the entity available property."""
self._attr_available = self._data.abode.events.connected
self.schedule_update_ha_state()
class AbodeDevice(AbodeEntity):
"""Representation of an Abode device."""
def __init__(self, data: AbodeSystem, device: AbodeDev) -> None:
"""Initialize Abode device."""
super().__init__(data)
self._device = device
self._attr_unique_id = device.uuid
async def async_added_to_hass(self) -> None:
"""Subscribe to device events."""
await super().async_added_to_hass()
await self.hass.async_add_executor_job(
self._data.abode.events.add_device_callback,
self._device.id,
self._update_callback,
)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from device events."""
await super().async_will_remove_from_hass()
await self.hass.async_add_executor_job(
self._data.abode.events.remove_all_device_callbacks, self._device.id
)
def update(self) -> None:
"""Update device state."""
self._device.refresh()
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
return {
"device_id": self._device.id,
"battery_low": self._device.battery_low,
"no_response": self._device.no_response,
"device_type": self._device.type,
}
@property
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self._device.id)},
manufacturer="Abode",
model=self._device.type,
name=self._device.name,
)
def _update_callback(self, device: AbodeDev) -> None:
"""Update the device state."""
self.schedule_update_ha_state()
class AbodeAutomation(AbodeEntity):
"""Representation of an Abode automation."""
def __init__(self, data: AbodeSystem, automation: AbodeAuto) -> None:
"""Initialize for Abode automation."""
super().__init__(data)
self._automation = automation
self._attr_name = automation.name
self._attr_unique_id = automation.automation_id
self._attr_extra_state_attributes = {
"type": "CUE automation",
}
def update(self) -> None:
"""Update automation state."""
self._automation.refresh()

View File

@@ -5,10 +5,5 @@
"default": "mdi:robot" "default": "mdi:robot"
} }
} }
},
"services": {
"capture_image": "mdi:camera",
"change_setting": "mdi:cog",
"trigger_automation": "mdi:play"
} }
} }

View File

@@ -23,9 +23,8 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_temperature_mired_to_kelvin,
) )
from . import AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(

View File

@@ -10,9 +10,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
from .entity import AbodeDevice
async def async_setup_entry( async def async_setup_entry(

View File

@@ -27,9 +27,8 @@ from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeSystem from . import AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
from .entity import AbodeDevice
ABODE_TEMPERATURE_UNIT_HA_UNIT = { ABODE_TEMPERATURE_UNIT_HA_UNIT = {
UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,

View File

@@ -13,9 +13,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeSystem from . import AbodeAutomation, AbodeDevice, AbodeSystem
from .const import DOMAIN from .const import DOMAIN
from .entity import AbodeAutomation, AbodeDevice
DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE] DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE]

View File

@@ -2,10 +2,14 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from asyncio import timeout
from datetime import timedelta
import logging import logging
from typing import Any
from accuweather import AccuWeather from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -13,71 +17,43 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL_DAILY_FORECAST, UPDATE_INTERVAL_OBSERVATION from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER
from .coordinator import (
AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
@dataclass async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class AccuWeatherData:
"""Data for AccuWeather integration."""
coordinator_observation: AccuWeatherObservationDataUpdateCoordinator
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
"""Set up AccuWeather as config entry.""" """Set up AccuWeather as config entry."""
api_key: str = entry.data[CONF_API_KEY] api_key: str = entry.data[CONF_API_KEY]
name: str = entry.data[CONF_NAME] name: str = entry.data[CONF_NAME]
assert entry.unique_id is not None
location_key = entry.unique_id location_key = entry.unique_id
forecast: bool = entry.options.get(CONF_FORECAST, False)
_LOGGER.debug("Using location_key: %s", location_key) _LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast)
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
accuweather = AccuWeather(api_key, websession, location_key=location_key)
coordinator_observation = AccuWeatherObservationDataUpdateCoordinator( coordinator = AccuWeatherDataUpdateCoordinator(
hass, hass, websession, api_key, location_key, forecast, name
accuweather,
name,
"observation",
UPDATE_INTERVAL_OBSERVATION,
) )
await coordinator.async_config_entry_first_refresh()
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator( entry.async_on_unload(entry.add_update_listener(update_listener))
hass,
accuweather,
name,
"daily forecast",
UPDATE_INTERVAL_DAILY_FORECAST,
)
await coordinator_observation.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await coordinator_daily_forecast.async_config_entry_first_refresh()
entry.runtime_data = AccuWeatherData(
coordinator_observation=coordinator_observation,
coordinator_daily_forecast=coordinator_daily_forecast,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Remove ozone sensors from registry if they exist # Remove ozone sensors from registry if they exist
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
for day in range(5): for day in range(0, 5):
unique_id = f"{location_key}-ozone-{day}" unique_id = f"{coordinator.location_key}-ozone-{day}"
if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id): if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id):
_LOGGER.debug("Removing ozone sensor entity %s", entity_id) _LOGGER.debug("Removing ozone sensor entity %s", entity_id)
ent_reg.async_remove(entity_id) ent_reg.async_remove(entity_id)
@@ -85,8 +61,78 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
return True return True
async def async_unload_entry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass: HomeAssistant, entry: AccuWeatherConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener."""
await hass.config_entries.async_reload(entry.entry_id)
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # pylint: disable=hass-enforce-coordinator-module
"""Class to manage fetching AccuWeather data API."""
def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
location_key: str,
forecast: bool,
name: str,
) -> None:
"""Initialize."""
self.location_key = location_key
self.forecast = forecast
self.accuweather = AccuWeather(api_key, session, location_key=location_key)
self.device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, location_key)},
manufacturer=MANUFACTURER,
name=name,
# You don't need to provide specific details for the URL,
# so passing in _ characters is fine if the location key
# is correct
configuration_url=(
"http://accuweather.com/en/"
f"_/_/{location_key}/"
f"weather-forecast/{location_key}/"
),
)
# Enabling the forecast download increases the number of requests per data
# update, we use 40 minutes for current condition only and 80 minutes for
# current condition and forecast as update interval to not exceed allowed number
# of requests. We have 50 requests allowed per day, so we use 36 and leave 14 as
# a reserve for restarting HA.
update_interval = timedelta(minutes=40)
if self.forecast:
update_interval *= 2
_LOGGER.debug("Data will be update every %s", update_interval)
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
forecast: list[dict[str, Any]] = []
try:
async with timeout(10):
current = await self.accuweather.async_get_current_conditions()
if self.forecast:
forecast = await self.accuweather.async_get_daily_forecast()
except (
ApiError,
ClientConnectorError,
InvalidApiKeyError,
RequestsExceededError,
) as error:
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return {**current, **{ATTR_FORECAST: forecast}}

View File

@@ -10,12 +10,26 @@ from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
)
from .const import DOMAIN from .const import CONF_FORECAST, DOMAIN
OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_FORECAST, default=False): bool,
}
)
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
}
class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN): class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -73,3 +87,9 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
), ),
errors=errors, errors=errors,
) )
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
"""Options callback for AccuWeather."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)

View File

@@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from typing import Final from typing import Final
from homeassistant.components.weather import ( from homeassistant.components.weather import (
@@ -28,8 +27,10 @@ ATTR_CATEGORY: Final = "Category"
ATTR_DIRECTION: Final = "Direction" ATTR_DIRECTION: Final = "Direction"
ATTR_ENGLISH: Final = "English" ATTR_ENGLISH: Final = "English"
ATTR_LEVEL: Final = "level" ATTR_LEVEL: Final = "level"
ATTR_FORECAST: Final = "forecast"
ATTR_SPEED: Final = "Speed" ATTR_SPEED: Final = "Speed"
ATTR_VALUE: Final = "Value" ATTR_VALUE: Final = "Value"
CONF_FORECAST: Final = "forecast"
DOMAIN: Final = "accuweather" DOMAIN: Final = "accuweather"
MANUFACTURER: Final = "AccuWeather, Inc." MANUFACTURER: Final = "AccuWeather, Inc."
MAX_FORECAST_DAYS: Final = 4 MAX_FORECAST_DAYS: Final = 4
@@ -55,5 +56,3 @@ CONDITION_MAP = {
for cond_ha, cond_codes in CONDITION_CLASSES.items() for cond_ha, cond_codes in CONDITION_CLASSES.items()
for cond_code in cond_codes for cond_code in cond_codes
} }
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)

View File

@@ -1,124 +0,0 @@
"""The AccuWeather coordinator."""
from asyncio import timeout
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp.client_exceptions import ClientConnectorError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
TimestampDataUpdateCoordinator,
UpdateFailed,
)
from .const import DOMAIN, MANUFACTURER
EXCEPTIONS = (ApiError, ClientConnectorError, InvalidApiKeyError, RequestsExceededError)
_LOGGER = logging.getLogger(__name__)
class AccuWeatherObservationDataUpdateCoordinator(
DataUpdateCoordinator[dict[str, Any]]
):
"""Class to manage fetching AccuWeather data API."""
def __init__(
self,
hass: HomeAssistant,
accuweather: AccuWeather,
name: str,
coordinator_type: str,
update_interval: timedelta,
) -> None:
"""Initialize."""
self.accuweather = accuweather
self.location_key = accuweather.location_key
if TYPE_CHECKING:
assert self.location_key is not None
self.device_info = _get_device_info(self.location_key, name)
super().__init__(
hass,
_LOGGER,
name=f"{name} ({coordinator_type})",
update_interval=update_interval,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
async with timeout(10):
result = await self.accuweather.async_get_current_conditions()
except EXCEPTIONS as error:
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return result
class AccuWeatherDailyForecastDataUpdateCoordinator(
TimestampDataUpdateCoordinator[list[dict[str, Any]]]
):
"""Class to manage fetching AccuWeather data API."""
def __init__(
self,
hass: HomeAssistant,
accuweather: AccuWeather,
name: str,
coordinator_type: str,
update_interval: timedelta,
) -> None:
"""Initialize."""
self.accuweather = accuweather
self.location_key = accuweather.location_key
if TYPE_CHECKING:
assert self.location_key is not None
self.device_info = _get_device_info(self.location_key, name)
super().__init__(
hass,
_LOGGER,
name=f"{name} ({coordinator_type})",
update_interval=update_interval,
)
async def _async_update_data(self) -> list[dict[str, Any]]:
"""Update data via library."""
try:
async with timeout(10):
result = await self.accuweather.async_get_daily_forecast()
except EXCEPTIONS as error:
raise UpdateFailed(error) from error
_LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining)
return result
def _get_device_info(location_key: str, name: str) -> DeviceInfo:
"""Get device info."""
return DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, location_key)},
manufacturer=MANUFACTURER,
name=name,
# You don't need to provide specific details for the URL,
# so passing in _ characters is fine if the location key
# is correct
configuration_url=(
"http://accuweather.com/en/"
f"_/_/{location_key}/weather-forecast/{location_key}/"
),
)

View File

@@ -5,21 +5,27 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AccuWeatherConfigEntry, AccuWeatherData from . import AccuWeatherDataUpdateCoordinator
from .const import DOMAIN
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: AccuWeatherConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
accuweather_data: AccuWeatherData = config_entry.runtime_data coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
return { diagnostics_data = {
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
"observation_data": accuweather_data.coordinator_observation.data, "coordinator_data": coordinator.data,
} }
return diagnostics_data

View File

@@ -8,6 +8,6 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["accuweather"], "loggers": ["accuweather"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["accuweather==3.0.0"], "requirements": ["accuweather==2.1.1"],
"single_config_entry": true "single_config_entry": true
} }

View File

@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_CUBIC_METER,
PERCENTAGE, PERCENTAGE,
@@ -27,22 +28,20 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherConfigEntry from . import AccuWeatherDataUpdateCoordinator
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_CATEGORY, ATTR_CATEGORY,
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_ENGLISH, ATTR_ENGLISH,
ATTR_FORECAST,
ATTR_LEVEL, ATTR_LEVEL,
ATTR_SPEED, ATTR_SPEED,
ATTR_VALUE, ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
DOMAIN,
MAX_FORECAST_DAYS, MAX_FORECAST_DAYS,
) )
from .coordinator import (
AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator,
)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@@ -53,18 +52,12 @@ class AccuWeatherSensorDescription(SensorEntityDescription):
value_fn: Callable[[dict[str, Any]], str | int | float | None] value_fn: Callable[[dict[str, Any]], str | int | float | None]
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
day: int | None = None
@dataclass(frozen=True, kw_only=True) FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
class AccuWeatherForecastSensorDescription(AccuWeatherSensorDescription):
"""Class describing AccuWeather sensor entities."""
day: int
FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="AirQuality", key="AirQuality",
icon="mdi:air-filter", icon="mdi:air-filter",
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
@@ -76,7 +69,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="CloudCoverDay", key="CloudCoverDay",
icon="mdi:weather-cloudy", icon="mdi:weather-cloudy",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -88,7 +81,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="CloudCoverNight", key="CloudCoverNight",
icon="mdi:weather-cloudy", icon="mdi:weather-cloudy",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -100,7 +93,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="Grass", key="Grass",
icon="mdi:grass", icon="mdi:grass",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -113,7 +106,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="HoursOfSun", key="HoursOfSun",
icon="mdi:weather-partly-cloudy", icon="mdi:weather-partly-cloudy",
native_unit_of_measurement=UnitOfTime.HOURS, native_unit_of_measurement=UnitOfTime.HOURS,
@@ -124,7 +117,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="LongPhraseDay", key="LongPhraseDay",
value_fn=lambda data: cast(str, data), value_fn=lambda data: cast(str, data),
translation_key=f"condition_day_{day}d", translation_key=f"condition_day_{day}d",
@@ -133,7 +126,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="LongPhraseNight", key="LongPhraseNight",
value_fn=lambda data: cast(str, data), value_fn=lambda data: cast(str, data),
translation_key=f"condition_night_{day}d", translation_key=f"condition_night_{day}d",
@@ -142,7 +135,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="Mold", key="Mold",
icon="mdi:blur", icon="mdi:blur",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -155,7 +148,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="Ragweed", key="Ragweed",
icon="mdi:sprout", icon="mdi:sprout",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
@@ -168,7 +161,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureMax", key="RealFeelTemperatureMax",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -179,7 +172,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureMin", key="RealFeelTemperatureMin",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -190,7 +183,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMax", key="RealFeelTemperatureShadeMax",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -202,7 +195,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMin", key="RealFeelTemperatureShadeMin",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -214,7 +207,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="SolarIrradianceDay", key="SolarIrradianceDay",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -226,7 +219,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="SolarIrradianceNight", key="SolarIrradianceNight",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -238,7 +231,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="ThunderstormProbabilityDay", key="ThunderstormProbabilityDay",
icon="mdi:weather-lightning", icon="mdi:weather-lightning",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
@@ -249,7 +242,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="ThunderstormProbabilityNight", key="ThunderstormProbabilityNight",
icon="mdi:weather-lightning", icon="mdi:weather-lightning",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
@@ -260,7 +253,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="Tree", key="Tree",
icon="mdi:tree-outline", icon="mdi:tree-outline",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
@@ -273,7 +266,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="UVIndex", key="UVIndex",
icon="mdi:weather-sunny", icon="mdi:weather-sunny",
native_unit_of_measurement=UV_INDEX, native_unit_of_measurement=UV_INDEX,
@@ -285,7 +278,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="WindGustDay", key="WindGustDay",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -298,7 +291,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="WindGustNight", key="WindGustNight",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
@@ -311,7 +304,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="WindDay", key="WindDay",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
@@ -323,7 +316,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
for day in range(MAX_FORECAST_DAYS + 1) for day in range(MAX_FORECAST_DAYS + 1)
), ),
*( *(
AccuWeatherForecastSensorDescription( AccuWeatherSensorDescription(
key="WindNight", key="WindNight",
device_class=SensorDeviceClass.WIND_SPEED, device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
@@ -456,36 +449,29 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
entry: AccuWeatherConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add AccuWeather entities from a config_entry.""" """Add AccuWeather entities from a config_entry."""
observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = (
entry.runtime_data.coordinator_observation
)
forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = (
entry.runtime_data.coordinator_daily_forecast
)
sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [ coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
AccuWeatherSensor(observation_coordinator, description)
for description in SENSOR_TYPES sensors = [
AccuWeatherSensor(coordinator, description) for description in SENSOR_TYPES
] ]
sensors.extend( if coordinator.forecast:
[ for description in FORECAST_SENSOR_TYPES:
AccuWeatherForecastSensor(forecast_daily_coordinator, description) # Some air quality/allergy sensors are only available for certain
for description in FORECAST_SENSOR_TYPES # locations.
if description.key in forecast_daily_coordinator.data[description.day] if description.key not in coordinator.data[ATTR_FORECAST][description.day]:
] continue
) sensors.append(AccuWeatherSensor(coordinator, description))
async_add_entities(sensors) async_add_entities(sensors)
class AccuWeatherSensor( class AccuWeatherSensor(
CoordinatorEntity[AccuWeatherObservationDataUpdateCoordinator], SensorEntity CoordinatorEntity[AccuWeatherDataUpdateCoordinator], SensorEntity
): ):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
@@ -495,15 +481,22 @@ class AccuWeatherSensor(
def __init__( def __init__(
self, self,
coordinator: AccuWeatherObservationDataUpdateCoordinator, coordinator: AccuWeatherDataUpdateCoordinator,
description: AccuWeatherSensorDescription, description: AccuWeatherSensorDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self.forecast_day = description.day
self.entity_description = description self.entity_description = description
self._sensor_data = self._get_sensor_data(coordinator.data, description.key) self._sensor_data = _get_sensor_data(
self._attr_unique_id = f"{coordinator.location_key}-{description.key}".lower() coordinator.data, description.key, self.forecast_day
)
if self.forecast_day is not None:
self._attr_unique_id = f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower()
else:
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}".lower()
)
self._attr_device_info = coordinator.device_info self._attr_device_info = coordinator.device_info
@property @property
@@ -514,78 +507,30 @@ class AccuWeatherSensor(
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if self.forecast_day is not None:
return self.entity_description.attr_fn(self._sensor_data)
return self.entity_description.attr_fn(self.coordinator.data) return self.entity_description.attr_fn(self.coordinator.data)
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle data update.""" """Handle data update."""
self._sensor_data = self._get_sensor_data( self._sensor_data = _get_sensor_data(
self.coordinator.data, self.entity_description.key
)
self.async_write_ha_state()
@staticmethod
def _get_sensor_data(
sensors: dict[str, Any],
kind: str,
) -> Any:
"""Get sensor data."""
if kind == "Precipitation":
return sensors["PrecipitationSummary"]["PastHour"]
return sensors[kind]
class AccuWeatherForecastSensor(
CoordinatorEntity[AccuWeatherDailyForecastDataUpdateCoordinator], SensorEntity
):
"""Define an AccuWeather entity."""
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
entity_description: AccuWeatherForecastSensorDescription
def __init__(
self,
coordinator: AccuWeatherDailyForecastDataUpdateCoordinator,
description: AccuWeatherForecastSensorDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self.forecast_day = description.day
self.entity_description = description
self._sensor_data = self._get_sensor_data(
coordinator.data, description.key, self.forecast_day
)
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower()
)
self._attr_device_info = coordinator.device_info
@property
def native_value(self) -> str | int | float | None:
"""Return the state."""
return self.entity_description.value_fn(self._sensor_data)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return self.entity_description.attr_fn(self._sensor_data)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle data update."""
self._sensor_data = self._get_sensor_data(
self.coordinator.data, self.entity_description.key, self.forecast_day self.coordinator.data, self.entity_description.key, self.forecast_day
) )
self.async_write_ha_state() self.async_write_ha_state()
@staticmethod
def _get_sensor_data( def _get_sensor_data(
sensors: list[dict[str, Any]], sensors: dict[str, Any],
kind: str, kind: str,
forecast_day: int, forecast_day: int | None = None,
) -> Any: ) -> Any:
"""Get sensor data.""" """Get sensor data."""
return sensors[forecast_day][kind] if forecast_day is not None:
return sensors[ATTR_FORECAST][forecast_day][kind]
if kind == "Precipitation":
return sensors["PrecipitationSummary"]["PastHour"]
return sensors[kind]

View File

@@ -11,7 +11,7 @@
} }
}, },
"create_entry": { "create_entry": {
"default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration." "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options."
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -790,6 +790,16 @@
} }
} }
}, },
"options": {
"step": {
"init": {
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.",
"data": {
"forecast": "Weather forecast"
}
}
}
},
"system_health": { "system_health": {
"info": { "info": {
"can_reach_server": "Reach AccuWeather server", "can_reach_server": "Reach AccuWeather server",

View File

@@ -9,7 +9,6 @@ from accuweather.const import ENDPOINT
from homeassistant.components import system_health from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from . import AccuWeatherConfigEntry
from .const import DOMAIN from .const import DOMAIN
@@ -23,11 +22,9 @@ def async_register(
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"""Get info for the info page.""" """Get info for the info page."""
config_entry: AccuWeatherConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] remaining_requests = list(hass.data[DOMAIN].values())[
0
remaining_requests = ( ].accuweather.requests_remaining
config_entry.runtime_data.coordinator_observation.accuweather.requests_remaining
)
return { return {
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT), "can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),

View File

@@ -17,10 +17,11 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TIME, ATTR_FORECAST_TIME,
ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_UV_INDEX,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
CoordinatorWeatherEntity,
Forecast, Forecast,
SingleCoordinatorWeatherEntity,
WeatherEntityFeature, WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
UnitOfLength, UnitOfLength,
UnitOfPrecipitationDepth, UnitOfPrecipitationDepth,
@@ -32,153 +33,125 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherConfigEntry, AccuWeatherData from . import AccuWeatherDataUpdateCoordinator
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_DIRECTION, ATTR_DIRECTION,
ATTR_FORECAST,
ATTR_SPEED, ATTR_SPEED,
ATTR_VALUE, ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
CONDITION_MAP, CONDITION_MAP,
) DOMAIN,
from .coordinator import (
AccuWeatherDailyForecastDataUpdateCoordinator,
AccuWeatherObservationDataUpdateCoordinator,
) )
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
entry: AccuWeatherConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add a AccuWeather weather entity from a config_entry.""" """Add a AccuWeather weather entity from a config_entry."""
async_add_entities([AccuWeatherEntity(entry.runtime_data)])
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([AccuWeatherEntity(coordinator)])
class AccuWeatherEntity( class AccuWeatherEntity(
CoordinatorWeatherEntity[ SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator]
AccuWeatherObservationDataUpdateCoordinator,
AccuWeatherDailyForecastDataUpdateCoordinator,
]
): ):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_name = None _attr_name = None
def __init__(self, accuweather_data: AccuWeatherData) -> None: def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
"""Initialize.""" """Initialize."""
super().__init__( super().__init__(coordinator)
observation_coordinator=accuweather_data.coordinator_observation,
daily_coordinator=accuweather_data.coordinator_daily_forecast,
)
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_pressure_unit = UnitOfPressure.HPA
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_native_visibility_unit = UnitOfLength.KILOMETERS self._attr_native_visibility_unit = UnitOfLength.KILOMETERS
self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR self._attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
self._attr_unique_id = accuweather_data.coordinator_observation.location_key self._attr_unique_id = coordinator.location_key
self._attr_attribution = ATTRIBUTION self._attr_attribution = ATTRIBUTION
self._attr_device_info = accuweather_data.coordinator_observation.device_info self._attr_device_info = coordinator.device_info
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY if self.coordinator.forecast:
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
self.observation_coordinator = accuweather_data.coordinator_observation
self.daily_coordinator = accuweather_data.coordinator_daily_forecast
@property @property
def condition(self) -> str | None: def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
return CONDITION_MAP.get(self.observation_coordinator.data["WeatherIcon"]) return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"])
@property @property
def cloud_coverage(self) -> float: def cloud_coverage(self) -> float:
"""Return the Cloud coverage in %.""" """Return the Cloud coverage in %."""
return cast(float, self.observation_coordinator.data["CloudCover"]) return cast(float, self.coordinator.data["CloudCover"])
@property @property
def native_apparent_temperature(self) -> float: def native_apparent_temperature(self) -> float:
"""Return the apparent temperature.""" """Return the apparent temperature."""
return cast( return cast(
float, float, self.coordinator.data["ApparentTemperature"][API_METRIC][ATTR_VALUE]
self.observation_coordinator.data["ApparentTemperature"][API_METRIC][
ATTR_VALUE
],
) )
@property @property
def native_temperature(self) -> float: def native_temperature(self) -> float:
"""Return the temperature.""" """Return the temperature."""
return cast( return cast(float, self.coordinator.data["Temperature"][API_METRIC][ATTR_VALUE])
float,
self.observation_coordinator.data["Temperature"][API_METRIC][ATTR_VALUE],
)
@property @property
def native_pressure(self) -> float: def native_pressure(self) -> float:
"""Return the pressure.""" """Return the pressure."""
return cast( return cast(float, self.coordinator.data["Pressure"][API_METRIC][ATTR_VALUE])
float, self.observation_coordinator.data["Pressure"][API_METRIC][ATTR_VALUE]
)
@property @property
def native_dew_point(self) -> float: def native_dew_point(self) -> float:
"""Return the dew point.""" """Return the dew point."""
return cast( return cast(float, self.coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE])
float, self.observation_coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE]
)
@property @property
def humidity(self) -> int: def humidity(self) -> int:
"""Return the humidity.""" """Return the humidity."""
return cast(int, self.observation_coordinator.data["RelativeHumidity"]) return cast(int, self.coordinator.data["RelativeHumidity"])
@property @property
def native_wind_gust_speed(self) -> float: def native_wind_gust_speed(self) -> float:
"""Return the wind gust speed.""" """Return the wind gust speed."""
return cast( return cast(
float, float, self.coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ATTR_VALUE]
self.observation_coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][
ATTR_VALUE
],
) )
@property @property
def native_wind_speed(self) -> float: def native_wind_speed(self) -> float:
"""Return the wind speed.""" """Return the wind speed."""
return cast( return cast(
float, float, self.coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ATTR_VALUE]
self.observation_coordinator.data["Wind"][ATTR_SPEED][API_METRIC][
ATTR_VALUE
],
) )
@property @property
def wind_bearing(self) -> int: def wind_bearing(self) -> int:
"""Return the wind bearing.""" """Return the wind bearing."""
return cast( return cast(int, self.coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"])
int, self.observation_coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"]
)
@property @property
def native_visibility(self) -> float: def native_visibility(self) -> float:
"""Return the visibility.""" """Return the visibility."""
return cast( return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE])
float,
self.observation_coordinator.data["Visibility"][API_METRIC][ATTR_VALUE],
)
@property @property
def uv_index(self) -> float: def uv_index(self) -> float:
"""Return the UV index.""" """Return the UV index."""
return cast(float, self.observation_coordinator.data["UVIndex"]) return cast(float, self.coordinator.data["UVIndex"])
@callback @property
def _async_forecast_daily(self) -> list[Forecast] | None: def forecast(self) -> list[Forecast] | None:
"""Return the daily forecast in native units.""" """Return the forecast array."""
if not self.coordinator.forecast:
return None
# remap keys from library to keys understood by the weather component
return [ return [
{ {
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
@@ -202,5 +175,10 @@ class AccuWeatherEntity(
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]), ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]),
} }
for item in self.daily_coordinator.data for item in self.coordinator.data[ATTR_FORECAST]
] ]
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast in native units."""
return self.forecast

View File

@@ -4,35 +4,30 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .hub import PulseHub from .hub import PulseHub
CONF_HUBS = "hubs" CONF_HUBS = "hubs"
PLATFORMS = [Platform.COVER, Platform.SENSOR] PLATFORMS = [Platform.COVER, Platform.SENSOR]
type AcmedaConfigEntry = ConfigEntry[PulseHub]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, config_entry: AcmedaConfigEntry
) -> bool:
"""Set up Rollease Acmeda Automate hub from a config entry.""" """Set up Rollease Acmeda Automate hub from a config entry."""
hub = PulseHub(hass, config_entry) hub = PulseHub(hass, config_entry)
if not await hub.async_setup(): if not await hub.async_setup():
return False return False
config_entry.runtime_data = hub hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True return True
async def async_unload_entry( async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
hass: HomeAssistant, config_entry: AcmedaConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
hub = config_entry.runtime_data hub = hass.data[DOMAIN][config_entry.entry_id]
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS config_entry, PLATFORMS
@@ -41,4 +36,7 @@ async def async_unload_entry(
if not await hub.async_reset(): if not await hub.async_reset():
return False return False
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok return unload_ok

View File

@@ -9,23 +9,24 @@ from homeassistant.components.cover import (
CoverEntity, CoverEntity,
CoverEntityFeature, CoverEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AcmedaConfigEntry
from .base import AcmedaBase from .base import AcmedaBase
from .const import ACMEDA_HUB_UPDATE from .const import ACMEDA_HUB_UPDATE, DOMAIN
from .helpers import async_add_acmeda_entities from .helpers import async_add_acmeda_entities
from .hub import PulseHub
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AcmedaConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Acmeda Rollers from a config entry.""" """Set up the Acmeda Rollers from a config entry."""
hub = config_entry.runtime_data hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id]
current: set[int] = set() current: set[int] = set()

View File

@@ -2,8 +2,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from aiopulse import Roller from aiopulse import Roller
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -13,20 +11,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
if TYPE_CHECKING:
from . import AcmedaConfigEntry
@callback @callback
def async_add_acmeda_entities( def async_add_acmeda_entities(
hass: HomeAssistant, hass: HomeAssistant,
entity_class: type, entity_class: type,
config_entry: AcmedaConfigEntry, config_entry: ConfigEntry,
current: set[int], current: set[int],
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add any new entities.""" """Add any new entities."""
hub = config_entry.runtime_data hub = hass.data[DOMAIN][config_entry.entry_id]
LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host) LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host)
api = hub.api.rollers api = hub.api.rollers

View File

@@ -3,24 +3,25 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AcmedaConfigEntry
from .base import AcmedaBase from .base import AcmedaBase
from .const import ACMEDA_HUB_UPDATE from .const import ACMEDA_HUB_UPDATE, DOMAIN
from .helpers import async_add_acmeda_entities from .helpers import async_add_acmeda_entities
from .hub import PulseHub
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AcmedaConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Acmeda Rollers from a config entry.""" """Set up the Acmeda Rollers from a config entry."""
hub = config_entry.runtime_data hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id]
current: set[int] = set() current: set[int] = set()

View File

@@ -9,7 +9,7 @@ from typing import Final
LEASES_REGEX: Final[re.Pattern[str]] = re.compile( LEASES_REGEX: Final[re.Pattern[str]] = re.compile(
r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})" r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})"
r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" + r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
r"\svalid\sfor:\s(?P<timevalid>(-?\d+))" + r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
r"\ssec" + r"\ssec"
) )

View File

@@ -135,15 +135,11 @@ class AdaxDevice(ClimateEntity):
class LocalAdaxDevice(ClimateEntity): class LocalAdaxDevice(ClimateEntity):
"""Representation of a heater.""" """Representation of a heater."""
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_modes = [HVACMode.HEAT]
_attr_hvac_mode = HVACMode.HEAT _attr_hvac_mode = HVACMode.HEAT
_attr_max_temp = 35 _attr_max_temp = 35
_attr_min_temp = 5 _attr_min_temp = 5
_attr_supported_features = ( _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_target_temperature_step = PRECISION_WHOLE _attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
@@ -156,14 +152,6 @@ class LocalAdaxDevice(ClimateEntity):
manufacturer="Adax", manufacturer="Adax",
) )
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode."""
if hvac_mode == HVACMode.HEAT:
temperature = self._attr_target_temperature or self._attr_min_temp
await self._adax_data_handler.set_target_temperature(temperature)
elif hvac_mode == HVACMode.OFF:
await self._adax_data_handler.set_target_temperature(0)
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
@@ -173,14 +161,6 @@ class LocalAdaxDevice(ClimateEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Get the latest data.""" """Get the latest data."""
data = await self._adax_data_handler.get_status() data = await self._adax_data_handler.get_status()
self._attr_target_temperature = data["target_temperature"]
self._attr_current_temperature = data["current_temperature"] self._attr_current_temperature = data["current_temperature"]
self._attr_available = self._attr_current_temperature is not None self._attr_available = self._attr_current_temperature is not None
if (target_temp := data["target_temperature"]) == 0:
self._attr_hvac_mode = HVACMode.OFF
self._attr_icon = "mdi:radiator-off"
if target_temp == 0:
self._attr_target_temperature = self._attr_min_temp
else:
self._attr_hvac_mode = HVACMode.HEAT
self._attr_icon = "mdi:radiator"
self._attr_target_temperature = target_temp

View File

@@ -2,12 +2,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
@@ -26,6 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ( from .const import (
CONF_FORCE, CONF_FORCE,
DATA_ADGUARD_CLIENT,
DOMAIN, DOMAIN,
SERVICE_ADD_URL, SERVICE_ADD_URL,
SERVICE_DISABLE_URL, SERVICE_DISABLE_URL,
@@ -43,18 +42,9 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
) )
PLATFORMS = [Platform.SENSOR, Platform.SWITCH] PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
@dataclass async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class AdGuardData:
"""Adguard data type."""
client: AdGuardHome
version: str
async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
"""Set up AdGuard Home from a config entry.""" """Set up AdGuard Home from a config entry."""
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
adguard = AdGuardHome( adguard = AdGuardHome(
@@ -67,13 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
session=session, session=session,
) )
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ADGUARD_CLIENT: adguard}
try: try:
version = await adguard.version() await adguard.version()
except AdGuardHomeConnectionError as exception: except AdGuardHomeConnectionError as exception:
raise ConfigEntryNotReady from exception raise ConfigEntryNotReady from exception
entry.runtime_data = AdGuardData(adguard, version)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def add_url(call: ServiceCall) -> None: async def add_url(call: ServiceCall) -> None:
@@ -117,20 +107,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload AdGuard Home config entry.""" """Unload AdGuard Home config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
loaded_entries = [ if unload_ok:
entry hass.data[DOMAIN].pop(entry.entry_id)
for entry in hass.config_entries.async_entries(DOMAIN) if not hass.data[DOMAIN]:
if entry.state == ConfigEntryState.LOADED
]
if len(loaded_entries) == 1:
# This is the last loaded instance of AdGuard, deregister any services
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH) hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
del hass.data[DOMAIN]
return unload_ok return unload_ok

View File

@@ -6,6 +6,9 @@ DOMAIN = "adguard"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
DATA_ADGUARD_CLIENT = "adguard_client"
DATA_ADGUARD_VERSION = "adguard_version"
CONF_FORCE = "force" CONF_FORCE = "force"
SERVICE_ADD_URL = "add_url" SERVICE_ADD_URL = "add_url"

View File

@@ -2,14 +2,13 @@
from __future__ import annotations from __future__ import annotations
from adguardhome import AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeError
from homeassistant.config_entries import SOURCE_HASSIO from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import AdGuardConfigEntry, AdGuardData from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER
from .const import DOMAIN, LOGGER
class AdGuardHomeEntity(Entity): class AdGuardHomeEntity(Entity):
@@ -20,13 +19,12 @@ class AdGuardHomeEntity(Entity):
def __init__( def __init__(
self, self,
data: AdGuardData, adguard: AdGuardHome,
entry: AdGuardConfigEntry, entry: ConfigEntry,
) -> None: ) -> None:
"""Initialize the AdGuard Home entity.""" """Initialize the AdGuard Home entity."""
self._entry = entry self._entry = entry
self.data = data self.adguard = adguard
self.adguard = data.client
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update AdGuard Home entity.""" """Update AdGuard Home entity."""
@@ -46,7 +44,7 @@ class AdGuardHomeEntity(Entity):
async def _adguard_update(self) -> None: async def _adguard_update(self) -> None:
"""Update AdGuard Home entity.""" """Update AdGuard Home entity."""
raise NotImplementedError raise NotImplementedError()
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
@@ -70,6 +68,8 @@ class AdGuardHomeEntity(Entity):
}, },
manufacturer="AdGuard Team", manufacturer="AdGuard Team",
name="AdGuard Home", name="AdGuard Home",
sw_version=self.data.version, sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get(
DATA_ADGUARD_VERSION
),
configuration_url=config_url, configuration_url=config_url,
) )

View File

@@ -7,15 +7,16 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any
from adguardhome import AdGuardHome from adguardhome import AdGuardHome, AdGuardHomeConnectionError
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfTime from homeassistant.const import PERCENTAGE, UnitOfTime
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardConfigEntry, AdGuardData from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN
from .const import DOMAIN
from .entity import AdGuardHomeEntity from .entity import AdGuardHomeEntity
SCAN_INTERVAL = timedelta(seconds=300) SCAN_INTERVAL = timedelta(seconds=300)
@@ -84,14 +85,21 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AdGuardConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdGuard Home sensor based on a config entry.""" """Set up AdGuard Home sensor based on a config entry."""
data = entry.runtime_data adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT]
try:
version = await adguard.version()
except AdGuardHomeConnectionError as exception:
raise PlatformNotReady from exception
hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version
async_add_entities( async_add_entities(
[AdGuardHomeSensor(data, entry, description) for description in SENSORS], [AdGuardHomeSensor(adguard, entry, description) for description in SENSORS],
True, True,
) )
@@ -103,18 +111,18 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
def __init__( def __init__(
self, self,
data: AdGuardData, adguard: AdGuardHome,
entry: AdGuardConfigEntry, entry: ConfigEntry,
description: AdGuardHomeEntityDescription, description: AdGuardHomeEntityDescription,
) -> None: ) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__(data, entry) super().__init__(adguard, entry)
self.entity_description = description self.entity_description = description
self._attr_unique_id = "_".join( self._attr_unique_id = "_".join(
[ [
DOMAIN, DOMAIN,
self.adguard.host, adguard.host,
str(self.adguard.port), str(adguard.port),
"sensor", "sensor",
description.key, description.key,
] ]

View File

@@ -7,14 +7,15 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardConfigEntry, AdGuardData from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN, LOGGER
from .const import DOMAIN, LOGGER
from .entity import AdGuardHomeEntity from .entity import AdGuardHomeEntity
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
@@ -78,14 +79,21 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: AdGuardConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdGuard Home switch based on a config entry.""" """Set up AdGuard Home switch based on a config entry."""
data = entry.runtime_data adguard = hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_CLIENT]
try:
version = await adguard.version()
except AdGuardHomeConnectionError as exception:
raise PlatformNotReady from exception
hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version
async_add_entities( async_add_entities(
[AdGuardHomeSwitch(data, entry, description) for description in SWITCHES], [AdGuardHomeSwitch(adguard, entry, description) for description in SWITCHES],
True, True,
) )
@@ -97,21 +105,15 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
def __init__( def __init__(
self, self,
data: AdGuardData, adguard: AdGuardHome,
entry: AdGuardConfigEntry, entry: ConfigEntry,
description: AdGuardHomeSwitchEntityDescription, description: AdGuardHomeSwitchEntityDescription,
) -> None: ) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""
super().__init__(data, entry) super().__init__(adguard, entry)
self.entity_description = description self.entity_description = description
self._attr_unique_id = "_".join( self._attr_unique_id = "_".join(
[ [DOMAIN, adguard.host, str(adguard.port), "switch", description.key]
DOMAIN,
self.adguard.host,
str(self.adguard.port),
"switch",
description.key,
]
) )
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:

View File

@@ -1,5 +0,0 @@
{
"services": {
"write_data_by_name": "mdi:pencil"
}
}

View File

@@ -12,11 +12,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ADVANTAGE_AIR_RETRY from .const import ADVANTAGE_AIR_RETRY, DOMAIN
from .models import AdvantageAirData from .models import AdvantageAirData
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirData]
ADVANTAGE_AIR_SYNC_INTERVAL = 15 ADVANTAGE_AIR_SYNC_INTERVAL = 15
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
@@ -33,9 +31,7 @@ _LOGGER = logging.getLogger(__name__)
REQUEST_REFRESH_DELAY = 0.5 REQUEST_REFRESH_DELAY = 0.5
async def async_setup_entry( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass: HomeAssistant, entry: AdvantageAirDataConfigEntry
) -> bool:
"""Set up Advantage Air config.""" """Set up Advantage Air config."""
ip_address = entry.data[CONF_IP_ADDRESS] ip_address = entry.data[CONF_IP_ADDRESS]
port = entry.data[CONF_PORT] port = entry.data[CONF_PORT]
@@ -65,15 +61,19 @@ async def async_setup_entry(
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.runtime_data = AdvantageAirData(coordinator, api) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = AdvantageAirData(coordinator, api)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass: HomeAssistant, entry: AdvantageAirDataConfigEntry
) -> bool:
"""Unload Advantage Air Config.""" """Unload Advantage Air Config."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -6,11 +6,12 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -19,12 +20,12 @@ PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir Binary Sensor platform.""" """Set up AdvantageAir Binary Sensor platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
entities: list[BinarySensorEntity] = [] entities: list[BinarySensorEntity] = []
if aircons := instance.coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):

View File

@@ -16,18 +16,19 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ( from .const import (
ADVANTAGE_AIR_AUTOFAN_ENABLED, ADVANTAGE_AIR_AUTOFAN_ENABLED,
ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_CLOSE,
ADVANTAGE_AIR_STATE_OFF, ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON, ADVANTAGE_AIR_STATE_ON,
ADVANTAGE_AIR_STATE_OPEN, ADVANTAGE_AIR_STATE_OPEN,
DOMAIN as ADVANTAGE_AIR_DOMAIN,
) )
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -57,7 +58,7 @@ HVAC_MODES = [
HVACMode.FAN_ONLY, HVACMode.FAN_ONLY,
HVACMode.DRY, HVACMode.DRY,
] ]
HVAC_MODES_MYAUTO = [*HVAC_MODES, HVACMode.HEAT_COOL] HVAC_MODES_MYAUTO = HVAC_MODES + [HVACMode.HEAT_COOL]
SUPPORTED_FEATURES = ( SUPPORTED_FEATURES = (
ClimateEntityFeature.FAN_MODE ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_OFF
@@ -75,12 +76,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir climate platform.""" """Set up AdvantageAir climate platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
entities: list[ClimateEntity] = [] entities: list[ClimateEntity] = []
if aircons := instance.coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):

View File

@@ -45,7 +45,7 @@ class AdvantageAirConfigFlow(ConfigFlow, domain=DOMAIN):
port=port, port=port,
session=async_get_clientsession(self.hass), session=async_get_clientsession(self.hass),
retry=ADVANTAGE_AIR_RETRY, retry=ADVANTAGE_AIR_RETRY,
).async_get() ).async_get(1)
except ApiError: except ApiError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: else:

View File

@@ -8,11 +8,15 @@ from homeassistant.components.cover import (
CoverEntity, CoverEntity,
CoverEntityFeature, CoverEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry from .const import (
from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN ADVANTAGE_AIR_STATE_CLOSE,
ADVANTAGE_AIR_STATE_OPEN,
DOMAIN as ADVANTAGE_AIR_DOMAIN,
)
from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -21,12 +25,12 @@ PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir cover platform.""" """Set up AdvantageAir cover platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
entities: list[CoverEntity] = [] entities: list[CoverEntity] = []
if aircons := instance.coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):

View File

@@ -5,9 +5,10 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AdvantageAirDataConfigEntry from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
TO_REDACT = [ TO_REDACT = [
"dealerPhoneNumber", "dealerPhoneNumber",
@@ -24,10 +25,10 @@ TO_REDACT = [
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: AdvantageAirDataConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
data = config_entry.runtime_data.coordinator.data data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id].coordinator.data
# Return only the relevant children # Return only the relevant children
return { return {

View File

@@ -1,5 +0,0 @@
{
"services": {
"set_time_to": "mdi:timer-cog"
}
}

View File

@@ -3,11 +3,11 @@
from typing import Any from typing import Any
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirEntity, AdvantageAirThingEntity from .entity import AdvantageAirEntity, AdvantageAirThingEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -15,12 +15,12 @@ from .models import AdvantageAirData
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir light platform.""" """Set up AdvantageAir light platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
entities: list[LightEntity] = [] entities: list[LightEntity] = []
if my_lights := instance.coordinator.data.get("myLights"): if my_lights := instance.coordinator.data.get("myLights"):

View File

@@ -1,10 +1,11 @@
"""Select platform for Advantage Air integration.""" """Select platform for Advantage Air integration."""
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirAcEntity from .entity import AdvantageAirAcEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -13,12 +14,12 @@ ADVANTAGE_AIR_INACTIVE = "Inactive"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir select platform.""" """Set up AdvantageAir select platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
if aircons := instance.coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):
async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons) async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons)

View File

@@ -12,13 +12,13 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN
from .const import ADVANTAGE_AIR_STATE_OPEN
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -31,12 +31,12 @@ PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir sensor platform.""" """Set up AdvantageAir sensor platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
entities: list[SensorEntity] = [] entities: list[SensorEntity] = []
if aircons := instance.coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):

View File

@@ -3,14 +3,15 @@
from typing import Any from typing import Any
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ( from .const import (
ADVANTAGE_AIR_AUTOFAN_ENABLED, ADVANTAGE_AIR_AUTOFAN_ENABLED,
ADVANTAGE_AIR_STATE_OFF, ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON, ADVANTAGE_AIR_STATE_ON,
DOMAIN as ADVANTAGE_AIR_DOMAIN,
) )
from .entity import AdvantageAirAcEntity, AdvantageAirThingEntity from .entity import AdvantageAirAcEntity, AdvantageAirThingEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -18,12 +19,12 @@ from .models import AdvantageAirData
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir switch platform.""" """Set up AdvantageAir switch platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
entities: list[SwitchEntity] = [] entities: list[SwitchEntity] = []
if aircons := instance.coordinator.data.get("aircons"): if aircons := instance.coordinator.data.get("aircons"):

View File

@@ -1,11 +1,11 @@
"""Advantage Air Update platform.""" """Advantage Air Update platform."""
from homeassistant.components.update import UpdateEntity from homeassistant.components.update import UpdateEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirEntity from .entity import AdvantageAirEntity
from .models import AdvantageAirData from .models import AdvantageAirData
@@ -13,12 +13,12 @@ from .models import AdvantageAirData
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdvantageAir update platform.""" """Set up AdvantageAir update platform."""
instance = config_entry.runtime_data instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
async_add_entities([AdvantageAirApp(instance)]) async_add_entities([AdvantageAirApp(instance)])

View File

@@ -1,6 +1,5 @@
"""The AEMET OpenData component.""" """The AEMET OpenData component."""
from dataclasses import dataclass
import logging import logging
from aemet_opendata.exceptions import AemetError, TownNotFound from aemet_opendata.exceptions import AemetError, TownNotFound
@@ -12,23 +11,19 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import CONF_STATION_UPDATES, PLATFORMS from .const import (
CONF_STATION_UPDATES,
DOMAIN,
ENTRY_NAME,
ENTRY_WEATHER_COORDINATOR,
PLATFORMS,
)
from .coordinator import WeatherUpdateCoordinator from .coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type AemetConfigEntry = ConfigEntry[AemetData]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@dataclass
class AemetData:
"""Aemet runtime data."""
name: str
coordinator: WeatherUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
"""Set up AEMET OpenData as config entry.""" """Set up AEMET OpenData as config entry."""
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
@@ -49,7 +44,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
weather_coordinator = WeatherUpdateCoordinator(hass, aemet) weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
await weather_coordinator.async_config_entry_first_refresh() await weather_coordinator.async_config_entry_first_refresh()
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
ENTRY_NAME: name,
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -65,4 +64,9 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -55,6 +55,8 @@ CONF_STATION_UPDATES = "station_updates"
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
DEFAULT_NAME = "AEMET" DEFAULT_NAME = "AEMET"
DOMAIN = "aemet" DOMAIN = "aemet"
ENTRY_NAME = "name"
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
ATTR_API_CONDITION = "condition" ATTR_API_CONDITION = "condition"
ATTR_API_FORECAST_CONDITION = "condition" ATTR_API_FORECAST_CONDITION = "condition"
@@ -119,3 +121,8 @@ FORECAST_MAP = {
AOD_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, AOD_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED,
}, },
} }
WEATHER_FORECAST_MODES = {
AOD_FORECAST_DAILY: "daily",
AOD_FORECAST_HOURLY: "hourly",
}

View File

@@ -7,6 +7,7 @@ from typing import Any
from aemet_opendata.const import AOD_COORDS from aemet_opendata.const import AOD_COORDS
from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,
@@ -15,7 +16,8 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AemetConfigEntry from .const import DOMAIN, ENTRY_WEATHER_COORDINATOR
from .coordinator import WeatherUpdateCoordinator
TO_REDACT_CONFIG = [ TO_REDACT_CONFIG = [
CONF_API_KEY, CONF_API_KEY,
@@ -30,10 +32,11 @@ TO_REDACT_COORD = [
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: AemetConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator = config_entry.runtime_data.coordinator aemet_entry = hass.data[DOMAIN][config_entry.entry_id]
coordinator: WeatherUpdateCoordinator = aemet_entry[ENTRY_WEATHER_COORDINATOR]
return { return {
"api_data": coordinator.aemet.raw_data(), "api_data": coordinator.aemet.raw_data(),

View File

@@ -56,7 +56,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import AemetConfigEntry
from .const import ( from .const import (
ATTR_API_CONDITION, ATTR_API_CONDITION,
ATTR_API_FORECAST_CONDITION, ATTR_API_FORECAST_CONDITION,
@@ -88,6 +87,9 @@ from .const import (
ATTR_API_WIND_SPEED, ATTR_API_WIND_SPEED,
ATTRIBUTION, ATTRIBUTION,
CONDITIONS_MAP, CONDITIONS_MAP,
DOMAIN,
ENTRY_NAME,
ENTRY_WEATHER_COORDINATOR,
) )
from .coordinator import WeatherUpdateCoordinator from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity from .entity import AemetEntity
@@ -358,13 +360,13 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AemetConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AEMET OpenData sensor entities based on a config entry.""" """Set up AEMET OpenData sensor entities based on a config entry."""
domain_data = config_entry.runtime_data domain_data = hass.data[DOMAIN][config_entry.entry_id]
name = domain_data.name name: str = domain_data[ENTRY_NAME]
coordinator = domain_data.coordinator coordinator: WeatherUpdateCoordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
async_add_entities( async_add_entities(
AemetSensor( AemetSensor(

View File

@@ -14,10 +14,12 @@ from aemet_opendata.const import (
) )
from homeassistant.components.weather import ( from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN,
Forecast, Forecast,
SingleCoordinatorWeatherEntity, SingleCoordinatorWeatherEntity,
WeatherEntityFeature, WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
UnitOfPrecipitationDepth, UnitOfPrecipitationDepth,
UnitOfPressure, UnitOfPressure,
@@ -25,28 +27,55 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AemetConfigEntry from .const import (
from .const import ATTRIBUTION, CONDITIONS_MAP ATTRIBUTION,
CONDITIONS_MAP,
DOMAIN,
ENTRY_NAME,
ENTRY_WEATHER_COORDINATOR,
WEATHER_FORECAST_MODES,
)
from .coordinator import WeatherUpdateCoordinator from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity from .entity import AemetEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AemetConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AEMET OpenData weather entity based on a config entry.""" """Set up AEMET OpenData weather entity based on a config entry."""
domain_data = config_entry.runtime_data domain_data = hass.data[DOMAIN][config_entry.entry_id]
name = domain_data.name weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
weather_coordinator = domain_data.coordinator
async_add_entities( entities = []
[AemetWeather(name, config_entry.unique_id, weather_coordinator)], entity_registry = er.async_get(hass)
False,
) # Add daily + hourly entity for legacy config entries, only add daily for new
# config entries. This can be removed in HA Core 2024.3
if entity_registry.async_get_entity_id(
WEATHER_DOMAIN,
DOMAIN,
f"{config_entry.unique_id} {WEATHER_FORECAST_MODES[AOD_FORECAST_HOURLY]}",
):
for mode, mode_id in WEATHER_FORECAST_MODES.items():
name = f"{domain_data[ENTRY_NAME]} {mode_id}"
unique_id = f"{config_entry.unique_id} {mode_id}"
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
else:
entities.append(
AemetWeather(
domain_data[ENTRY_NAME],
config_entry.unique_id,
weather_coordinator,
AOD_FORECAST_DAILY,
)
)
async_add_entities(entities, False)
class AemetWeather( class AemetWeather(
@@ -69,9 +98,14 @@ class AemetWeather(
name, name,
unique_id, unique_id,
coordinator: WeatherUpdateCoordinator, coordinator: WeatherUpdateCoordinator,
forecast_mode,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self._forecast_mode = forecast_mode
self._attr_entity_registry_enabled_default = (
self._forecast_mode == AOD_FORECAST_DAILY
)
self._attr_name = name self._attr_name = name
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
@@ -81,6 +115,11 @@ class AemetWeather(
cond = self.get_aemet_value([AOD_WEATHER, AOD_CONDITION]) cond = self.get_aemet_value([AOD_WEATHER, AOD_CONDITION])
return CONDITIONS_MAP.get(cond) return CONDITIONS_MAP.get(cond)
@property
def forecast(self) -> list[Forecast]:
"""Return the forecast array."""
return self.get_aemet_forecast(self._forecast_mode)
@callback @callback
def _async_forecast_daily(self) -> list[Forecast]: def _async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units.""" """Return the daily forecast in native units."""

View File

@@ -10,14 +10,16 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
type AfterShipConfigEntry = ConfigEntry[AfterShip]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) -> bool:
"""Set up AfterShip from a config entry.""" """Set up AfterShip from a config entry."""
hass.data.setdefault(DOMAIN, {})
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
aftership = AfterShip(api_key=entry.data[CONF_API_KEY], session=session) aftership = AfterShip(api_key=entry.data[CONF_API_KEY], session=session)
@@ -26,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) ->
except AfterShipException as err: except AfterShipException as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
entry.runtime_data = aftership hass.data[DOMAIN][entry.entry_id] = aftership
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -35,4 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) ->
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -8,6 +8,7 @@ from typing import Any, Final
from pyaftership import AfterShip, AfterShipException from pyaftership import AfterShip, AfterShipException
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@@ -17,7 +18,6 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import AfterShipConfigEntry
from .const import ( from .const import (
ADD_TRACKING_SERVICE_SCHEMA, ADD_TRACKING_SERVICE_SCHEMA,
ATTR_TRACKINGS, ATTR_TRACKINGS,
@@ -41,11 +41,11 @@ PLATFORM_SCHEMA: Final = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AfterShipConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AfterShip sensor entities based on a config entry.""" """Set up AfterShip sensor entities based on a config entry."""
aftership = config_entry.runtime_data aftership: AfterShip = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities([AfterShipSensor(aftership, config_entry.title)], True) async_add_entities([AfterShipSensor(aftership, config_entry.title)], True)

View File

@@ -10,20 +10,18 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL from .const import CONNECTION, DOMAIN as AGENT_DOMAIN, SERVER_URL
ATTRIBUTION = "ispyconnect.com" ATTRIBUTION = "ispyconnect.com"
DEFAULT_BRAND = "Agent DVR by ispyconnect.com" DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA] PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
AgentDVRConfigEntry = ConfigEntry[Agent]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
) -> bool:
"""Set up the Agent component.""" """Set up the Agent component."""
hass.data.setdefault(AGENT_DOMAIN, {})
server_origin = config_entry.data[SERVER_URL] server_origin = config_entry.data[SERVER_URL]
agent_client = Agent(server_origin, async_get_clientsession(hass)) agent_client = Agent(server_origin, async_get_clientsession(hass))
@@ -36,11 +34,9 @@ async def async_setup_entry(
if not agent_client.is_available: if not agent_client.is_available:
raise ConfigEntryNotReady raise ConfigEntryNotReady
config_entry.async_on_unload(agent_client.close)
await agent_client.get_devices() await agent_client.get_devices()
config_entry.runtime_data = agent_client hass.data[AGENT_DOMAIN][config_entry.entry_id] = {CONNECTION: agent_client}
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
@@ -58,8 +54,15 @@ async def async_setup_entry(
return True return True
async def async_unload_entry( async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close()
if unload_ok:
hass.data[AGENT_DOMAIN].pop(config_entry.entry_id)
return unload_ok

View File

@@ -6,6 +6,7 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity, AlarmControlPanelEntity,
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_HOME,
@@ -16,8 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AgentDVRConfigEntry from .const import CONNECTION, DOMAIN as AGENT_DOMAIN
from .const import DOMAIN as AGENT_DOMAIN
CONF_HOME_MODE_NAME = "home" CONF_HOME_MODE_NAME = "home"
CONF_AWAY_MODE_NAME = "away" CONF_AWAY_MODE_NAME = "away"
@@ -28,11 +28,13 @@ CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AgentDVRConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Agent DVR Alarm Control Panels.""" """Set up the Agent DVR Alarm Control Panels."""
async_add_entities([AgentBaseStation(config_entry.runtime_data)]) async_add_entities(
[AgentBaseStation(hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION])]
)
class AgentBaseStation(AlarmControlPanelEntity): class AgentBaseStation(AlarmControlPanelEntity):

View File

@@ -7,6 +7,7 @@ from agent import AgentError
from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import (
@@ -14,8 +15,12 @@ from homeassistant.helpers.entity_platform import (
async_get_current_platform, async_get_current_platform,
) )
from . import AgentDVRConfigEntry from .const import (
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN ATTRIBUTION,
CAMERA_SCAN_INTERVAL_SECS,
CONNECTION,
DOMAIN as AGENT_DOMAIN,
)
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS) SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
@@ -38,14 +43,14 @@ CAMERA_SERVICES = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: AgentDVRConfigEntry, config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Agent cameras.""" """Set up the Agent cameras."""
filter_urllib3_logging() filter_urllib3_logging()
cameras = [] cameras = []
server = config_entry.runtime_data server = hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION]
if not server.devices: if not server.devices:
_LOGGER.warning("Could not fetch cameras from Agent server") _LOGGER.warning("Could not fetch cameras from Agent server")
return return
@@ -75,11 +80,11 @@ class AgentCamera(MjpegCamera):
"""Initialize as a subclass of MjpegCamera.""" """Initialize as a subclass of MjpegCamera."""
self.device = device self.device = device
self._removed = False self._removed = False
self._attr_unique_id = f"{device.client.unique}_{device.typeID}_{device.id}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
super().__init__( super().__init__(
name=device.name, name=device.name,
mjpeg_url=f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001 mjpeg_url=f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001 still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
) )
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, self.unique_id)}, identifiers={(AGENT_DOMAIN, self.unique_id)},

View File

@@ -9,3 +9,4 @@ SERVICE_UPDATE = "update"
SIGNAL_UPDATE_AGENT = "agent_update" SIGNAL_UPDATE_AGENT = "agent_update"
ATTRIBUTION = "Data provided by ispyconnect.com" ATTRIBUTION = "Data provided by ispyconnect.com"
SERVER_URL = "server_url" SERVER_URL = "server_url"
CONNECTION = "connection"

View File

@@ -1,9 +0,0 @@
{
"services": {
"start_recording": "mdi:record-rec",
"stop_recording": "mdi:stop",
"enable_alerts": "mdi:bell-alert",
"disable_alerts": "mdi:bell-off",
"snapshot": "mdi:camera"
}
}

View File

@@ -17,9 +17,6 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.helpers.typing import ConfigType, StateType
from . import group as group_pre_import # noqa: F401
from .const import DOMAIN
_LOGGER: Final = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
ATTR_AQI: Final = "air_quality_index" ATTR_AQI: Final = "air_quality_index"
@@ -34,6 +31,8 @@ ATTR_PM_10: Final = "particulate_matter_10"
ATTR_PM_2_5: Final = "particulate_matter_2_5" ATTR_PM_2_5: Final = "particulate_matter_2_5"
ATTR_SO2: Final = "sulphur_dioxide" ATTR_SO2: Final = "sulphur_dioxide"
DOMAIN: Final = "air_quality"
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
SCAN_INTERVAL: Final = timedelta(seconds=30) SCAN_INTERVAL: Final = timedelta(seconds=30)
@@ -82,7 +81,7 @@ class AirQualityEntity(Entity):
@property @property
def particulate_matter_2_5(self) -> StateType: def particulate_matter_2_5(self) -> StateType:
"""Return the particulate matter 2.5 level.""" """Return the particulate matter 2.5 level."""
raise NotImplementedError raise NotImplementedError()
@property @property
def particulate_matter_10(self) -> StateType: def particulate_matter_10(self) -> StateType:

View File

@@ -1,5 +0,0 @@
"""Constants for the air_quality entity platform."""
from typing import Final
DOMAIN: Final = "air_quality"

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